Skip to main content

Event-Driven APIs: Webhooks, WebSockets, SSE, and Async Patterns

·APIScout Team
event drivenwebhookswebsocketssseasync api

Event-Driven APIs: Webhooks, WebSockets, SSE, and Async Patterns

REST APIs are request-response: the client asks, the server answers. But many real-world use cases need the server to push data to the client — payment confirmations, live chat, stock prices, build status updates. Event-driven APIs flip the model: the server notifies the client when something happens.

The Four Patterns

PatternDirectionConnectionBest For
WebhooksServer → ServerHTTP callbackBackend notifications, integrations
WebSocketsBidirectionalPersistent TCPChat, gaming, collaborative editing
SSEServer → ClientPersistent HTTPLive feeds, dashboards, notifications
Async Request-ReplyClient → Server → ClientPolling or callbackLong-running operations

1. Webhooks

How They Work

Your API sends an HTTP POST to the customer's URL when an event occurs:

Event occurs (e.g., payment succeeds)
  → Your API sends POST to customer's webhook URL
  → Customer's server processes the event
  → Customer returns 200 OK (acknowledgment)

Implementation

Provider side (sending webhooks):

1. Customer registers a webhook URL: https://their-app.com/webhooks
2. Event occurs in your system
3. Build webhook payload with event data
4. Sign the payload (HMAC-SHA256)
5. POST to customer's URL with signature header
6. If non-2xx response: retry with exponential backoff
7. After N failures: disable webhook, notify customer

Payload format:

{
  "id": "evt_a1b2c3d4",
  "type": "payment.succeeded",
  "created": "2026-03-08T14:30:00Z",
  "data": {
    "payment_id": "pay_xyz",
    "amount": 9900,
    "currency": "usd"
  }
}

Security

Sign every webhook so consumers can verify it came from you:

Signature = HMAC-SHA256(webhook_secret, raw_request_body)
Header: X-Webhook-Signature: sha256=a1b2c3d4...

Consumer verifies:

expected = HMAC-SHA256(their_webhook_secret, raw_body)
if (expected !== received_signature) reject

Additional security measures:

  • Include a timestamp to prevent replay attacks
  • Allow customers to rotate webhook secrets
  • Use HTTPS only (never send webhooks over HTTP)
  • Include an idempotency key so consumers can deduplicate

Retry Strategy

Attempt 1: Immediate
Attempt 2: 1 minute later
Attempt 3: 5 minutes later
Attempt 4: 30 minutes later
Attempt 5: 2 hours later
Attempt 6: 8 hours later
Attempt 7: 24 hours later → Give up, mark webhook as failing

After 3 consecutive days of failures: Disable the webhook and email the customer.

When to Use Webhooks

✅ Payment notifications, order updates, CI/CD build results, CRM events ❌ Real-time chat, live data streams, interactive experiences

2. WebSockets

How They Work

Persistent bidirectional connection over TCP:

Client: GET /ws (Upgrade: websocket)
Server: 101 Switching Protocols
── Connection established ──
Client → Server: { "type": "subscribe", "channel": "chat-123" }
Server → Client: { "type": "message", "text": "Hello!" }
Client → Server: { "type": "message", "text": "Hi back!" }
Server → Client: { "type": "message", "text": "How are you?" }
── Connection persists until explicitly closed ──

Implementation Considerations

Connection management:

ConcernSolution
Connection dropsAutomatic reconnect with backoff
AuthenticationAuth on initial handshake (token in query param or first message)
HeartbeatPing/pong every 30 seconds to detect dead connections
ScalingSticky sessions or pub/sub backplane (Redis)
Load balancingLayer 7 LB with WebSocket support (nginx, HAProxy)

Message format:

{
  "type": "event_type",
  "id": "msg_123",
  "timestamp": "2026-03-08T14:30:00Z",
  "data": { ... }
}

Scaling WebSockets

WebSocket connections are stateful — each connection lives on a specific server. Scaling requires:

Clients ↔ Load Balancer (sticky sessions)
             ↓
    Server 1    Server 2    Server 3
         ↓         ↓         ↓
         └─── Redis Pub/Sub ──┘
              (broadcast messages across servers)

Connection limits:

ScaleConnections per ServerServers Needed
Small10K1
Medium50K2-5
Large100K+10+ with auto-scaling

When to Use WebSockets

✅ Chat, gaming, collaborative editing, live trading, real-time multiplayer ❌ One-directional server pushes, infrequent updates, server-to-server

3. Server-Sent Events (SSE)

How They Work

One-directional stream from server to client over HTTP:

Client: GET /events (Accept: text/event-stream)
Server: 200 OK (Content-Type: text/event-stream)
── Stream opens ──
data: {"type": "update", "price": 150.25}

data: {"type": "update", "price": 150.30}

data: {"type": "update", "price": 149.95}
── Stream stays open indefinitely ──

SSE Format

event: price-update
id: 42
retry: 3000
data: {"symbol": "AAPL", "price": 150.25}

event: news
id: 43
data: {"headline": "Market opens higher"}
FieldPurpose
eventEvent type (for client-side filtering)
idLast event ID (for reconnection — "give me events after 43")
retryReconnection delay in milliseconds
dataEvent payload (can be multi-line)

SSE vs. WebSocket

DimensionSSEWebSocket
DirectionServer → Client onlyBidirectional
ProtocolHTTPWebSocket (TCP)
ReconnectionAutomatic (built into spec)Manual implementation
Data formatText onlyText or binary
Browser supportAll modern browsersAll modern browsers
Through proxiesWorks (it's HTTP)Sometimes blocked
ScalingStateless-friendlyRequires sticky sessions
Max connections6 per domain (HTTP/1.1), unlimited (HTTP/2)No limit

Use SSE when you only need server-to-client pushes. It's simpler, more reliable, and works better with existing HTTP infrastructure.

When to Use SSE

✅ Live dashboards, notification feeds, AI streaming responses, build logs, stock tickers ❌ Chat (need bidirectional), binary data, gaming

4. Async Request-Reply

How It Works

For long-running operations that can't return immediately:

Client: POST /api/reports/generate
Server: 202 Accepted
        { "job_id": "job_123", "status_url": "/api/jobs/job_123" }

── Client polls status ──
Client: GET /api/jobs/job_123
Server: { "status": "processing", "progress": 45 }

Client: GET /api/jobs/job_123
Server: { "status": "completed", "result_url": "/api/reports/abc.pdf" }

Implementation Patterns

Pattern 1: Polling

1. Client submits request → 202 Accepted with job ID
2. Client polls status endpoint every N seconds
3. Server returns progress updates
4. When complete: server returns result or download URL

Pattern 2: Webhook Callback

1. Client submits request with callback URL → 202 Accepted
2. Server processes asynchronously
3. Server POSTs result to callback URL when done
4. No polling needed

Pattern 3: WebSocket Notification

1. Client connects WebSocket and submits request
2. Server processes asynchronously
3. Server pushes progress updates and final result via WebSocket

Response Codes

CodeMeaningUse
202 AcceptedRequest accepted for processingInitial submission
200 OK with statusJob still processingStatus poll
303 See OtherJob complete, redirect to resultCompletion (with Location header)

When to Use Async Request-Reply

✅ Report generation, video processing, data imports, ML inference, batch operations ❌ Simple CRUD, instant responses, real-time interaction

Choosing the Right Pattern

Does the server need to notify the client?
  ├── No → Standard REST (request-response)
  └── Yes
      ├── Server-to-server notification?
      │   └── Webhooks
      ├── Client needs live updates?
      │   ├── Bidirectional? → WebSocket
      │   └── Server-to-client only? → SSE
      └── Long-running operation?
          └── Async Request-Reply

Pattern Comparison Summary

CriteriaWebhooksWebSocketSSEAsync Reply
ComplexityMediumHighLowMedium
InfrastructureSimpleComplex (stateful)SimpleMedium
Scaling difficultyLowHighMediumLow
LatencySecondsMillisecondsMillisecondsSeconds-minutes
ReliabilityRetry-basedConnection-dependentAuto-reconnectPolling-based
Firewall-friendlyYesSometimes noYesYes

Hybrid Architecture

Most production systems combine multiple patterns:

Payment API:
  - REST for creating charges (sync)
  - Webhooks for payment confirmations (async server-to-server)
  - SSE for dashboard live updates (async server-to-client)

Collaboration App:
  - REST for CRUD operations (sync)
  - WebSocket for real-time editing (bidirectional)
  - SSE for presence indicators (server-to-client)
  - Webhooks for third-party integrations (server-to-server)

Common Mistakes

MistakeImpactFix
Using WebSocket when SSE sufficesOver-engineered, harder to scaleSSE for server-push only
Polling instead of webhooksWasted resources, higher latencyImplement webhooks
No webhook retry logicLost events on temporary failuresExponential backoff + DLQ
WebSocket without heartbeatZombie connections consume resourcesPing/pong every 30 seconds
No reconnection logicDropped connections stay droppedAuto-reconnect with backoff
Unsigned webhooksSecurity vulnerabilityHMAC-SHA256 signatures

Building event-driven APIs? Explore async API patterns and tools on APIScout — comparisons, guides, and developer resources.

Comments