Webhook Approval Example

Complete example of a webhook receiver that processes approval requests.

Overview

This example shows how to set up a webhook endpoint to receive approval requests and send responses back to TraceMem.

Step 1: Create Webhook Integration

First, create a webhook integration via the Admin API:

text
import requests

# Admin API endpoint (requires JWT authentication)
admin_api_url = "https://api.tracemem.com"
jwt_token = "your-jwt-token"

# Create webhook integration
response = requests.post(
    f"{admin_api_url}/v1/admin/integrations/webhook",
    headers={"Authorization": f"Bearer {jwt_token}"},
    json={
        "name": "production-approvals",
        "url": "https://your-server.com/tracemem/webhook",
        "events": ["approval.requested", "approval.resolved"],
        "timeout_ms": 5000,
        "max_retries": 3
    }
)

result = response.json()
webhook_id = result["webhook_id"]
outgoing_secret = result["outgoing_secret"]  # For verifying incoming requests
incoming_secret = result["incoming_secret"]  # For authenticating responses

# Store these securely!

Step 2: Create Webhook Credential

Create a credential for sending approval responses:

text
# Create webhook credential
response = requests.post(
    f"{admin_api_url}/v1/admin/webhook-credentials",
    headers={"Authorization": f"Bearer {jwt_token}"},
    json={
        "name": "production-approvals",
        "expires_in": 31536000  # 1 year
    }
)

result = response.json()
credential_id = result["credential_id"]
webhook_secret = result["secret"]  # Use this to authenticate responses

# Store this securely!

Step 3: Webhook Receiver Server

Here's a complete webhook receiver that processes approval requests:

text
#!/usr/bin/env python3
"""
TraceMem Webhook Approval Receiver

Receives approval requests from TraceMem, processes them,
and sends approval responses back.
"""

import os
import sys
import json
import hmac
import hashlib
import argparse
from datetime import datetime
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
import queue
import requests


approval_queue = queue.Queue()


class ApprovalRequestHandler(BaseHTTPRequestHandler):
    """HTTP request handler for approval webhooks"""
    
    webhook_secret = None
    api_url = None
    
    def log_message(self, format, *args):
        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        sys.stderr.write(f"[{timestamp}] {format % args}\n")
    
    def do_POST(self):
        """Handle POST requests"""
        if self.path != '/webhook':
            self.send_error(404, "Not Found")
            return
        
        # Read request body
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length)
        
        # Parse JSON payload
        try:
            payload = json.loads(body.decode('utf-8'))
        except json.JSONDecodeError:
            self.send_error(400, "Invalid JSON")
            return
        
        # Validate event type
        if payload.get('event') != 'approval.requested':
            self.send_error(400, "Invalid event type")
            return
        
        # Add to queue for processing
        approval_queue.put({
            'payload': payload,
            'api_url': self.api_url,
            'webhook_secret': self.webhook_secret
        })
        
        # Send immediate success response
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps({"status": "queued"}).encode('utf-8'))


def approval_worker():
    """Worker thread that processes approval requests"""
    while True:
        item = approval_queue.get()
        payload = item['payload']
        api_url = item['api_url']
        webhook_secret = item['webhook_secret']
        
        try:
            process_approval(payload, api_url, webhook_secret)
        except Exception as e:
            print(f"Error processing approval: {e}")
        finally:
            approval_queue.task_done()


def process_approval(payload, api_url, webhook_secret):
    """Process approval request and prompt user (or make automatic decision)"""
    print("\n" + "="*80)
    print("APPROVAL REQUEST RECEIVED")
    print("="*80)
    print(f"Approval ID:  {payload.get('approval_id')}")
    print(f"Decision ID:  {payload.get('decision_id')}")
    print(f"Title:        {payload.get('title', 'N/A')}")
    print(f"Message:      {payload.get('message', 'N/A')}")
    print(f"Expires At:   {payload.get('expires_at', 'N/A')}")
    print("="*80)
    
    # Get approval decision (in production, this might be automatic or UI-based)
    decision = input("\nApprove or Reject? [approve/reject]: ").strip().lower()
    if decision not in ['approve', 'reject']:
        print("Invalid input. Defaulting to reject.")
        decision = 'reject'
    
    # Get rationale
    rationale = ""
    if payload.get('require_rationale'):
        rationale = input("Rationale (required): ").strip()
        if not rationale:
            print("Rationale is required. Defaulting to reject.")
            decision = 'reject'
            rationale = "No rationale provided"
    else:
        rationale = input("Rationale (optional): ").strip()
    
    # Send response to TraceMem API
    send_approval_response(
        api_url,
        webhook_secret,
        payload.get('token'),
        'approved' if decision == 'approve' else 'rejected',
        rationale
    )


def send_approval_response(api_url, webhook_secret, token, resolution, rationale):
    """Send approval response to TraceMem API"""
    url = f"{api_url}/v1/webhooks/approvals/respond"
    headers = {
        'Authorization': f'Bearer {webhook_secret}',
        'Content-Type': 'application/json',
    }
    
    payload = {
        'token': token,
        'resolution': resolution,
    }
    
    if rationale:
        payload['rationale'] = rationale
    
    print(f"\nSending {resolution} response to TraceMem API...")
    
    try:
        response = requests.post(url, headers=headers, json=payload, timeout=10)
        
        if response.status_code == 200:
            print(f"✅ Approval response sent successfully!")
            result = response.json()
            print(f"   Approval ID: {result.get('approval_id')}")
            print(f"   Decision ID: {result.get('decision_id')}")
            print(f"   Status: {result.get('status')}")
        else:
            print(f"❌ Error: HTTP {response.status_code}")
            print(f"   Response: {response.text}")
    
    except requests.exceptions.RequestException as e:
        print(f"❌ Error sending approval response: {e}")
    
    print("="*80 + "\n")


def main():
    parser = argparse.ArgumentParser(description='TraceMem Webhook Approval Receiver')
    
    parser.add_argument(
        '--secret',
        help='Webhook credential secret',
        default=os.environ.get('TRACEMEM_WEBHOOK_SECRET')
    )
    
    parser.add_argument(
        '--api-url',
        help='TraceMem API base URL',
        default=os.environ.get('TRACEMEM_API_URL', 'https://api.tracemem.com')
    )
    
    parser.add_argument(
        '--port',
        type=int,
        default=int(os.environ.get('WEBHOOK_PORT', 9000)),
        help='Port to listen on (default: 9000)'
    )
    
    args = parser.parse_args()
    
    if not args.secret:
        print("Error: Webhook credential secret is required", file=sys.stderr)
        sys.exit(1)
    
    # Set class variables
    ApprovalRequestHandler.webhook_secret = args.secret
    ApprovalRequestHandler.api_url = args.api_url
    
    # Start approval worker thread
    worker_thread = threading.Thread(target=approval_worker, daemon=True)
    worker_thread.start()
    
    # Start HTTP server
    server_address = ('0.0.0.0', args.port)
    httpd = HTTPServer(server_address, ApprovalRequestHandler)
    
    print("="*80)
    print("TraceMem Webhook Approval Receiver")
    print("="*80)
    print(f"Listening on: http://0.0.0.0:{args.port}/webhook")
    print(f"API URL:      {args.api_url}")
    print("="*80)
    print("\nWaiting for approval requests... (Press Ctrl+C to stop)\n")
    
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n\nShutting down server...")
        httpd.shutdown()
        print("Server stopped.")


if __name__ == '__main__':
    main()

Usage

Run the webhook receiver:

text
# Set environment variables
export TRACEMEM_WEBHOOK_SECRET="your-webhook-credential-secret"
export TRACEMEM_API_URL="https://api.tracemem.com"

# Run the server
python webhook_approval_receiver.py --port 9000

When an agent requests approval, TraceMem will POST to your webhook endpoint. The receiver processes the request, prompts for approval (or makes an automatic decision), and sends the response back to TraceMem.

TraceMem is trace-native infrastructure for AI agents