Skip to main content

Understanding Response Handling

When users interact with your ATP notifications, their responses need to be captured, validated, and processed securely. This guide covers everything you need to know about handling responses in your ATP integration.

The Response Lifecycle

The ATP response workflow follows these steps:

Setting Up Webhook Handlers

Your service will receive responses via webhooks. Here’s how to set up proper handlers:

Basic Webhook Handler

# Example webhook handler in Python using Flask
from flask import Flask, request, jsonify
import hmac
import hashlib
import json

app = Flask(__name__)

@app.route('/atp/webhook', methods=['POST'])
def handle_atp_webhook():
    # Get the webhook payload
    payload = request.json
    signature = request.headers.get('X-ATP-Signature')
    
    # Verify the signature (covered in Security section)
    if not verify_signature(payload, signature):
        return jsonify({"error": "Invalid signature"}), 401
    
    # Process different response types
    notification_id = payload['notification_id']
    action_id = payload['action_id']
    response_data = payload['response_data']
    
    # Handle the response based on your business logic
    process_response(notification_id, action_id, response_data)
    
    return jsonify({"status": "success"}), 200

def process_response(notification_id, action_id, response_data):
    # Your business logic here
    # Example: Update a database, trigger an action, etc.
    pass

Node.js Webhook Handler

// Example webhook handler in Node.js using Express
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

app.post('/atp/webhook', (req, res) => {
  const payload = req.body;
  const signature = req.headers['x-atp-signature'];
  
  // Verify the signature
  if (!verifySignature(payload, signature)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  const { notification_id, action_id, response_data } = payload;
  
  // Process the response
  processResponse(notification_id, action_id, response_data);
  
  return res.status(200).json({ status: 'success' });
});

function processResponse(notificationId, actionId, responseData) {
  // Your business logic here
}

Response Security

Security is critical when handling responses. Follow these best practices:

Signature Verification

Always verify that incoming webhooks are actually from the ATP server:
def verify_signature(payload, signature):
    # Your webhook secret from ATP service registration
    secret = "your_webhook_secret"
    
    # Create a signature using the payload and your secret
    expected_signature = hmac.new(
        secret.encode(),
        json.dumps(payload, sort_keys=True).encode(),
        hashlib.sha256
    ).hexdigest()
    
    # Compare signatures using a timing-safe comparison
    return hmac.compare_digest(expected_signature, signature)

Response Validation

Always validate the structure and content of responses:
def validate_response(response_data, expected_type):
    # Validate based on response type
    if expected_type == "simple":
        return True  # Simple responses don't need validation
    
    elif expected_type == "text":
        # Validate text response
        return isinstance(response_data, str) and len(response_data) <= 10000
    
    elif expected_type == "form":
        # Validate form data
        if not isinstance(response_data, dict):
            return False
        # Check required fields based on your form definition
        return all(k in response_data for k in required_fields)
    
    return False  # Unknown response type

Processing Different Response Types

ATP supports multiple response types. Here’s how to handle each:

Simple Responses

def handle_simple_response(notification_id, action_id):
    # Simple responses just indicate which action was selected
    if action_id == "approve":
        # Handle approval
        approve_requested_action(notification_id)
    elif action_id == "reject":
        # Handle rejection
        reject_requested_action(notification_id)

Text Responses

def handle_text_response(notification_id, action_id, response_data):
    # Text responses contain user-provided text
    user_input = response_data
    
    # Process the text input
    process_user_text_input(notification_id, user_input)

Form Responses

def handle_form_response(notification_id, action_id, response_data):
    # Form responses contain structured data
    form_data = response_data
    
    # Validate all required fields are present
    required_fields = ["name", "email", "reason"]
    if not all(field in form_data for field in required_fields):
        log_error(f"Missing required fields for notification {notification_id}")
        return
    
    # Process the form data
    process_form_submission(notification_id, form_data)

File Responses

def handle_file_response(notification_id, action_id, response_data):
    # File responses contain file metadata and download URLs
    file_info = response_data
    
    # Download and process the file
    file_url = file_info['download_url']
    file_type = file_info['mime_type']
    
    process_file(notification_id, file_url, file_type)

Response Workflow Integration

Integrate response handling into your application workflow:

State Management

def update_task_state(notification_id, new_state):
    # Update your database or state management system
    db.execute(
        "UPDATE tasks SET state = ? WHERE notification_id = ?",
        (new_state, notification_id)
    )
    
    # Optional: Update the notification status on the ATP server
    atp_client.update_notification_status(
        notification_id, 
        status="processed"
    )

Business Logic Triggers

def process_response(notification_id, action_id, response_data):
    # Retrieve the original task context
    task = db.get_task_by_notification_id(notification_id)
    
    if not task:
        log_error(f"Unknown notification ID: {notification_id}")
        return
    
    # Execute business logic based on the response
    if action_id == "approve":
        execute_approved_action(task)
        update_task_state(notification_id, "approved")
    
    elif action_id == "reject":
        handle_rejection(task, response_data.get("reason", ""))
        update_task_state(notification_id, "rejected")
    
    # Notify relevant systems about the state change
    trigger_system_updates(task, new_state=task.state)

Error Handling

Implement robust error handling for responses:

Handling Missing or Invalid Responses

def handle_webhook():
    try:
        # Process the webhook...
        
    except InvalidSignatureError:
        # Log security issues
        log_security_event("Invalid webhook signature detected")
        return jsonify({"error": "Invalid signature"}), 401
        
    except MalformedResponseError as e:
        # Log data validation issues
        log_error(f"Malformed response data: {str(e)}")
        return jsonify({"error": "Malformed data"}), 400
        
    except ResponseProcessingError as e:
        # Log business logic errors
        log_error(f"Failed to process response: {str(e)}")
        # Return 200 to acknowledge receipt, even though processing failed
        # This prevents ATP server from retrying
        return jsonify({"status": "received", "processing_error": str(e)}), 200

Retry Mechanisms

def handle_webhook_with_retry():
    try:
        # Process the webhook...
        
    except (DatabaseConnectionError, ExternalServiceError) as e:
        # Log the error
        log_error(f"Temporary error processing webhook: {str(e)}")
        
        # Return a 5XX status to trigger a retry from the ATP server
        return jsonify({"error": "Service temporarily unavailable"}), 503

Response Verification

Implement additional verification beyond signature checking:

Timestamp Verification

def verify_response_timestamp(payload):
    # Get the timestamp from the payload
    timestamp = payload.get('timestamp')
    
    if not timestamp:
        return False
    
    # Convert to datetime
    timestamp_dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
    
    # Check if the timestamp is recent (within 5 minutes)
    now = datetime.now(timezone.utc)
    time_diff = now - timestamp_dt
    
    return time_diff.total_seconds() < 300  # 5 minutes

Notification ID Verification

def verify_notification_id(notification_id):
    # Check if this is a notification your service actually sent
    result = db.execute(
        "SELECT COUNT(*) FROM sent_notifications WHERE id = ?", 
        (notification_id,)
    )
    
    return result.fetchone()[0] > 0

Advanced Response Processing

For more complex scenarios, implement these patterns:

Response Correlation

def correlate_response(notification_id):
    # Retrieve the original context
    original_request = db.get_request_by_notification_id(notification_id)
    
    if not original_request:
        log_error(f"Could not find original request for {notification_id}")
        return None
    
    # Return the correlation data
    return {
        "user_id": original_request.user_id,
        "request_id": original_request.id,
        "context": original_request.context,
        "timestamp": original_request.timestamp,
    }

Response Transformation

def transform_response_for_downstream(notification_id, action_id, response_data):
    # Transform the ATP response into the format expected by your systems
    correlation = correlate_response(notification_id)
    
    if not correlation:
        return None
    
    # Create a standardized internal event
    return {
        "event_type": "user_decision",
        "source": "atp",
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "user_id": correlation["user_id"],
        "request_id": correlation["request_id"],
        "decision": action_id,
        "additional_data": response_data,
        "metadata": {
            "notification_id": notification_id,
            "original_timestamp": correlation["timestamp"],
        }
    }

Best Practices

Follow these best practices for processing ATP responses:
  1. Idempotency: Design your webhook handlers to be idempotent to handle potential duplicate deliveries
  2. Logging: Maintain detailed logs of all received responses for troubleshooting and auditing
  3. Timeout Handling: Implement strategies for handling responses that arrive after your internal timeouts
  4. Acknowledgment: Always respond promptly to webhooks to acknowledge receipt
  5. Security First: Treat all incoming webhook data as untrusted until verified
  6. Database Transactions: Use transactions to ensure response processing is atomic

Verification: Knowledge Check

Before proceeding, let’s verify your understanding of response handling:

Troubleshooting

Common issues when processing responses:

Missing Responses

If you’re not receiving responses:
  1. Verify your webhook URL is correctly registered
  2. Check that your server is publicly accessible
  3. Look for firewall or security rules blocking incoming webhooks
  4. Verify the ATP server has the correct webhook URL for your service

Invalid Signatures

If signature verification fails:
  1. Confirm you’re using the correct webhook secret
  2. Ensure you’re calculating the signature in the same way as the ATP server
  3. Check for any middleware that might modify the request body before it reaches your handler

Processing Errors

If responses fail during processing:
  1. Improve error logging to capture the exact failure point
  2. Implement a dead letter queue for failed webhook processing
  3. Consider implementing a manual retry mechanism for critical responses

Next Steps

Now that you understand how to process responses securely, you’re ready to explore advanced ATP patterns and best practices.

Advanced ATP Patterns

Learn about decorator-based approaches, middleware integration, and more
I