🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Agent Integration Patterns: REST APIs, Webhooks & Event-Driven

AI AgentsAgent Integration Patterns🟢 Free Lesson

Advertisement

Agent Integration Patterns

Why This Matters

AI agents don't exist in isolation—they must communicate with databases, external APIs, user interfaces, and other services. Integration patterns define how agents connect to the world. Poor integration leads to fragile systems, data silos, and operational nightmares. Mastering these patterns enables agents that are composable, scalable, and resilient.

Real-World Analogy: Think of integration patterns like a city's transportation network. Roads (REST APIs) handle direct point-to-point travel, postal services (webhooks) deliver notifications asynchronously, and rail systems (event buses) move large volumes of data efficiently. A well-designed city uses all three depending on the need.

Integration Architecture Overview

Agent Integration PatternsREST API PatternGET /api/agents/{id}/statusPOST /api/agents/{id}/executePUT /api/agents/{id}/configSynchronous • Stateless • CacheableWebhook PatternPOST /webhooks/agent-eventsEvent: task.completedPayload: { result, metadata }Asynchronous • Push-based • EventualEvent-Driven PatternTopic: agent.tasksConsumer: agent-worker-1Partition: by agent_idDecoupled • Scalable • ResilientIntegration ComponentsAPI GatewayMessage BrokerEvent BusService MeshLoad BalancerAuthentication • Rate Limiting • Monitoring • Circuit Breaking • Retry LogicRequest Flow PatternClientRequestGatewayAuth + RouteAgentProcessToolsExternal APIsResponseReturnCacheStore

REST API Client

import asyncio
import hashlib
import json
import logging
import time
from dataclasses import dataclass, field
from typing import Any, Optional
from enum import Enum

logger = logging.getLogger(__name__)


class HTTPMethod(Enum):
    GET = "GET"
    POST = "POST"
    PUT = "PUT"
    DELETE = "DELETE"
    PATCH = "PATCH"


@dataclass
class APIConfig:
    base_url: str
    api_key: Optional[str] = None
    timeout: float = 30.0
    max_retries: int = 3
    retry_delay: float = 1.0
    rate_limit_rpm: int = 60
    headers: dict = field(default_factory=dict)


@dataclass
class APIResponse:
    status_code: int
    data: Any
    headers: dict
    latency: float
    cached: bool = False


class AgentRESTClient:
    def __init__(self, config: APIConfig):
        self.config = config
        self.request_count = 0
        self.error_count = 0
        self.cache: dict[str, tuple[Any, float]] = {}
        self.cache_ttl = 300.0
        self.rate_limit_timestamps: list[float] = []

    def _get_headers(self) -> dict:
        headers = {"Content-Type": "application/json", "User-Agent": "AgentSDK/1.0"}
        if self.config.api_key:
            headers["Authorization"] = f"Bearer {self.config.api_key}"
        headers.update(self.config.headers)
        return headers

    def _check_rate_limit(self) -> bool:
        now = time.time()
        self.rate_limit_timestamps = [ts for ts in self.rate_limit_timestamps if now - ts < 60]
        if len(self.rate_limit_timestamps) >= self.config.rate_limit_rpm:
            return False
        self.rate_limit_timestamps.append(now)
        return True

    def _get_cache_key(self, method: HTTPMethod, url: str, params: Optional[dict] = None) -> str:
        key_data = f"{method.value}:{url}:{json.dumps(params or {}, sort_keys=True)}"
        return hashlib.md5(key_data.encode()).hexdigest()

    async def request(
        self, method: HTTPMethod, endpoint: str, data: Any = None,
        params: Optional[dict] = None, use_cache: bool = True,
    ) -> APIResponse:
        if not self._check_rate_limit():
            await asyncio.sleep(1.0)
        if use_cache and method == HTTPMethod.GET:
            cache_key = self._get_cache_key(method, endpoint, params)
            if cache_key in self.cache:
                cached_data, cached_time = self.cache[cache_key]
                if time.time() - cached_time < self.cache_ttl:
                    return APIResponse(status_code=200, data=cached_data, headers={}, latency=0, cached=True)
        url = f"{self.config.base_url}{endpoint}"
        headers = self._get_headers()
        start_time = time.time()
        last_error = None
        for attempt in range(self.config.max_retries):
            try:
                self.request_count += 1
                await asyncio.sleep(0.01)
                response_data = {"status": "success", "data": data}
                latency = time.time() - start_time
                if use_cache and method == HTTPMethod.GET:
                    cache_key = self._get_cache_key(method, endpoint, params)
                    self.cache[cache_key] = (response_data, time.time())
                return APIResponse(status_code=200, data=response_data, headers=headers, latency=latency)
            except Exception as e:
                last_error = e
                self.error_count += 1
                if attempt < self.config.max_retries - 1:
                    await asyncio.sleep(self.config.retry_delay * (attempt + 1))
        raise last_error  # type: ignore

    async def get(self, endpoint: str, params: Optional[dict] = None) -> APIResponse:
        return await self.request(HTTPMethod.GET, endpoint, params=params)

    async def post(self, endpoint: str, data: Any = None) -> APIResponse:
        return await self.request(HTTPMethod.POST, endpoint, data=data, use_cache=False)

    async def put(self, endpoint: str, data: Any = None) -> APIResponse:
        return await self.request(HTTPMethod.PUT, endpoint, data=data, use_cache=False)

    async def delete(self, endpoint: str) -> APIResponse:
        return await self.request(HTTPMethod.DELETE, endpoint, use_cache=False)

    def get_stats(self) -> dict:
        return {
            "total_requests": self.request_count,
            "total_errors": self.error_count,
            "success_rate": (self.request_count - self.error_count) / self.request_count if self.request_count > 0 else 0,
            "cache_size": len(self.cache),
        }

Webhook Handler

import asyncio
import hashlib
import hmac
import json
import logging
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Optional
from enum import Enum

logger = logging.getLogger(__name__)


class WebhookEvent(Enum):
    TASK_COMPLETED = "task.completed"
    TASK_FAILED = "task.failed"
    AGENT_ERROR = "agent.error"
    TOOL_RESULT = "tool.result"
    STATE_CHANGED = "state.changed"


@dataclass
class WebhookConfig:
    secret: str
    max_retries: int = 3
    retry_delay: float = 1.0
    timeout: float = 10.0
    signature_header: str = "X-Webhook-Signature"


class WebhookHandler:
    def __init__(self, config: WebhookConfig):
        self.config = config
        self.handlers: dict[WebhookEvent, list[Callable]] = {}
        self.delivery_log: list[dict] = []
        self.failed_deliveries: list[dict] = []

    def register_handler(self, event: WebhookEvent, handler: Callable) -> None:
        if event not in self.handlers:
            self.handlers[event] = []
        self.handlers[event].append(handler)

    def _generate_signature(self, payload: str) -> str:
        return hmac.new(self.config.secret.encode(), payload.encode(), hashlib.sha256).hexdigest()

    def verify_signature(self, payload: str, signature: str) -> bool:
        expected = self._generate_signature(payload)
        return hmac.compare_digest(expected, signature)

    async def receive_webhook(self, payload: dict, signature: str) -> tuple[bool, str]:
        payload_str = json.dumps(payload, sort_keys=True)
        if not self.verify_signature(payload_str, signature):
            return False, "Invalid signature"
        event = WebhookEvent(payload.get("event"))
        handlers = self.handlers.get(event, [])
        if not handlers:
            return True, "No handlers registered"
        for handler in handlers:
            try:
                await asyncio.wait_for(handler(payload.get("data", {})), timeout=self.config.timeout)
            except asyncio.TimeoutError:
                self.failed_deliveries.append({"event": event.value, "timestamp": time.time(), "error": "Handler timeout"})
            except Exception as e:
                self.failed_deliveries.append({"event": event.value, "timestamp": time.time(), "error": str(e)})
        return True, "Delivered"

    async def send_webhook(self, url: str, event: WebhookEvent, data: dict) -> bool:
        payload_str = json.dumps({"event": event.value, "data": data, "timestamp": time.time()}, sort_keys=True)
        signature = self._generate_signature(payload_str)
        for attempt in range(self.config.max_retries):
            try:
                self.delivery_log.append({"url": url, "event": event.value, "timestamp": time.time(), "attempt": attempt + 1, "success": True})
                return True
            except Exception:
                if attempt < self.config.max_retries - 1:
                    await asyncio.sleep(self.config.retry_delay * (attempt + 1))
        self.failed_deliveries.append({"url": url, "event": event.value, "timestamp": time.time(), "error": "Max retries exceeded"})
        return False

    def get_stats(self) -> dict:
        total = len(self.delivery_log) + len(self.failed_deliveries)
        return {
            "total_deliveries": len(self.delivery_log),
            "failed_deliveries": len(self.failed_deliveries),
            "success_rate": len(self.delivery_log) / total if total else 0,
            "handlers_registered": sum(len(h) for h in self.handlers.values()),
        }

Event Bus System

import asyncio
import logging
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Callable, Optional
from enum import Enum

logger = logging.getLogger(__name__)


class EventType(Enum):
    AGENT_CREATED = "agent.created"
    AGENT_UPDATED = "agent.updated"
    TASK_ASSIGNED = "task.assigned"
    TASK_COMPLETED = "task.completed"
    TASK_FAILED = "task.failed"
    TOOL_REGISTERED = "tool.registered"
    ERROR_OCCURRED = "error.occurred"


@dataclass
class Event:
    id: str
    type: EventType
    data: dict
    timestamp: float
    source: str = ""
    metadata: dict = field(default_factory=dict)


@dataclass
class EventSubscription:
    event_type: EventType
    handler: Callable
    filter_fn: Optional[Callable] = None
    max_retries: int = 3


class EventBus:
    def __init__(self):
        self.subscriptions: dict[EventType, list[EventSubscription]] = {}
        self.event_history: list[Event] = []
        self.max_history_size = 10000
        self.processing_queue: asyncio.Queue = asyncio.Queue()
        self.is_running = False

    def subscribe(self, event_type: EventType, handler: Callable, filter_fn: Optional[Callable] = None) -> None:
        subscription = EventSubscription(event_type=event_type, handler=handler, filter_fn=filter_fn)
        if event_type not in self.subscriptions:
            self.subscriptions[event_type] = []
        self.subscriptions[event_type].append(subscription)

    async def publish(self, event_type: EventType, data: dict, source: str = "") -> str:
        event = Event(id=str(uuid.uuid4()), type=event_type, data=data, timestamp=time.time(), source=source)
        self.event_history.append(event)
        if len(self.event_history) > self.max_history_size:
            self.event_history = self.event_history[-self.max_history_size:]
        await self.processing_queue.put(event)
        return event.id

    async def start_processing(self) -> None:
        self.is_running = True
        while self.is_running:
            try:
                event = await asyncio.wait_for(self.processing_queue.get(), timeout=1.0)
                await self._process_event(event)
            except asyncio.TimeoutError:
                continue
            except Exception:
                continue

    async def _process_event(self, event: Event) -> None:
        subscriptions = self.subscriptions.get(event.type, [])
        for subscription in subscriptions:
            if subscription.filter_fn and not subscription.filter_fn(event):
                continue
            for attempt in range(subscription.max_retries):
                try:
                    await asyncio.wait_for(subscription.handler(event), timeout=10.0)
                    break
                except Exception:
                    if attempt < subscription.max_retries - 1:
                        await asyncio.sleep(0.1 * (attempt + 1))

    def stop_processing(self) -> None:
        self.is_running = False

    def get_event_history(self, event_type: Optional[EventType] = None, limit: int = 100) -> list[Event]:
        filtered = [e for e in self.event_history if e.type == event_type] if event_type else self.event_history
        return filtered[-limit:]

    def get_stats(self) -> dict:
        type_counts: dict[str, int] = {}
        for event in self.event_history:
            type_name = event.type.value
            type_counts[type_name] = type_counts.get(type_name, 0) + 1
        return {
            "total_events": len(self.event_history),
            "event_type_counts": type_counts,
            "subscriptions": sum(len(s) for s in self.subscriptions.values()),
            "queue_size": self.processing_queue.qsize(),
        }

Mathematical Foundations

API Reliability Score:

Webhook Delivery Rate:

Event Processing Latency:

Integration Throughput:

Retry Efficiency:

Performance Considerations

PatternLatencyThroughputCouplingBest For
REST API50-500msMediumTightSynchronous queries
Webhook100ms-5sHighLooseEvent notifications
Event Bus10-100msVery HighVery LooseDecoupled systems
Message Queue5-50msVery HighLooseBackground processing
GraphQL100-1000msMediumMediumComplex data queries
gRPC5-50msVery HighTightInternal service calls

Security Considerations

  • Authentication: Use OAuth2/JWT for user-facing APIs, API keys for service-to-service
  • Webhook verification: Always verify HMAC signatures on incoming webhooks
  • Rate limiting: Implement per-client rate limits to prevent abuse
  • Input validation: Validate and sanitize all incoming data before processing
  • HTTPS everywhere: Encrypt all API communications in transit
  • IP whitelisting: Restrict webhook sources to known IP ranges
  • Audit logging: Log all API calls for security monitoring and compliance
  • Secret rotation: Regularly rotate API keys and webhook secrets

Interview Questions

1. When should you use REST APIs vs webhooks for agent integration?

Answer: REST APIs are ideal for synchronous request-response patterns where the client needs immediate results. Use for: status checks, simple commands, CRUD operations. Webhooks are ideal for asynchronous notifications where the client doesn't need immediate response. Use for: event notifications, status updates, background task completion. REST is better for real-time needs; webhooks are better for event-driven architectures.

2. How do you handle webhook delivery failures?

Answer: Failure handling: 1) Implement retry with exponential backoff, 2) Use dead letter queues for failed deliveries, 3) Monitor delivery rates and set alerts, 4) Implement idempotency for retries, 5) Provide manual retry capabilities, 6) Log all failures for debugging. Consider: retry limits, timeout handling, and circuit breakers for downstream services.

3. What is the benefit of an event-driven architecture for agents?

Answer: Benefits: 1) Loose coupling between components, 2) Scalability through asynchronous processing, 3) Better fault isolation, 4) Easier to add new consumers, 5) Natural fit for agent workflows. Events enable: pub/sub patterns, event sourcing, CQRS, and reactive systems. Tradeoffs include eventual consistency and increased complexity.

4. How do you secure agent API integrations?

Answer: Security measures: 1) OAuth2/JWT for authentication, 2) API keys for service-to-service, 3) Rate limiting to prevent abuse, 4) Input validation and sanitization, 5) HTTPS everywhere, 6) Webhook signature verification, 7) IP whitelisting for webhooks, 8) Audit logging. Implement defense in depth—never rely on single security control.

5. How do you handle API versioning in agent systems?

Answer: Versioning strategies: 1) URL versioning (/v1/agents), 2) Header versioning (Accept-Version), 3) Query parameter versioning, 4) Content negotiation. Best practices: maintain backward compatibility, deprecate gradually, document changes, provide migration guides. For agents: version tool schemas, response formats, and API contracts separately.

6. What are common integration failure patterns?

Answer: Common failures: 1) Network timeouts, 2) Rate limiting, 3) Authentication failures, 4) Data format mismatches, 5) Service unavailability, 6) Partial responses, 7) Duplicate deliveries. Mitigation: circuit breakers, retry logic, fallback mechanisms, idempotency, and comprehensive monitoring.

7. How do you monitor integration health?

Answer: Monitoring: 1) Track API response times and error rates, 2) Monitor webhook delivery success, 3) Alert on circuit breaker trips, 4) Track event processing latency, 5) Monitor queue depths, 6) Set up synthetic monitoring. Key metrics: latency, throughput, error rates, and availability.

8. How do you test integration patterns?

Answer: Testing approach: 1) Unit tests for individual components, 2) Integration tests for API contracts, 3) Contract testing for API compatibility, 4) Load testing for performance, 5) Chaos testing for resilience, 6) End-to-end tests for complete flows. Use mocks for external services and test failure scenarios explicitly.

Common Pitfalls

PitfallSolution
No retry logicImplement exponential backoff
Ignoring rate limitsTrack and respect limits
No idempotencyDesign for duplicate handling
Synchronous blockingUse async for non-critical paths
No circuit breakersImplement for all external calls
Missing monitoringTrack all integration metrics
Ignoring versioningPlan for API evolution
No securityImplement authentication everywhere

Summary with Key Takeaways

  • REST APIs are best for synchronous, request-response patterns
  • Webhooks enable asynchronous, push-based notifications
  • Event-driven architectures provide scalability and loose coupling
  • Circuit breakers prevent cascading failures across integrations
  • Retry logic with backoff handles transient failures
  • Monitoring is essential for integration health visibility
  • Security must be implemented at every integration point
  • Testing should cover contracts, performance, and failure scenarios

KnowledgeCheck

  1. When should you use REST APIs over webhooks?

    • a) For asynchronous notifications
    • b) For synchronous request-response
    • c) For event-driven systems
    • d) For all integrations
  2. What is the purpose of webhook signature verification?

    • a) Improve performance
    • b) Ensure payload authenticity
    • c) Reduce latency
    • d) Increase throughput
  3. What is the benefit of event-driven architecture?

    • a) Simpler implementation
    • b) Loose coupling between components
    • c) Synchronous processing
    • d) Lower costs
  4. What should webhook retry logic use?

    • a) Fixed delay
    • b) Exponential backoff
    • c) No retries
    • d) Random delay
  5. What metric measures API reliability?

    • a) Latency
    • b) Success rate
    • c) Throughput
    • d) Cost
  6. Why implement circuit breakers for integrations?

    • a) Improve performance
    • b) Prevent cascading failures
    • c) Reduce costs
    • d) Increase accuracy

Answers: 1-b, 2-b, 3-b, 4-b, 5-b, 6-b

Need Expert AI Agents Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement