Skip to main content
Follow these best practices to build a production-ready webhook integration that handles high volume, recovers from failures, and provides a great developer experience.

Architecture Patterns

Decouple Receipt from Processing

Return 200 immediately, process asynchronously. The most critical pattern for reliable webhook handling is separating acknowledgment from processing.
1

Receive the webhook

Your endpoint receives the POST request from Smartcar
2

Persist immediately

Write the raw payload to a queue, database, or object storage
3

Return 200

Acknowledge receipt with a 200 status code (within 15 seconds)
4

Process asynchronously

A background worker processes the persisted payload
Why this matters:
  • Prevents timeouts from slow business logic
  • Allows you to retry processing without requesting redelivery
  • Enables you to update processing logic without losing historical events
  • Survives temporary outages in downstream systems
from flask import Flask, request
from rq import Queue
from redis import Redis

app = Flask(__name__)
redis_conn = Redis()
queue = Queue(connection=redis_conn)

@app.post("/webhooks/smartcar")
def webhook_handler():
    # 1. Get the raw payload
    payload = request.get_json()
    
    # 2. Queue for processing
    queue.enqueue(process_webhook, payload)
    
    # 3. Return immediately
    return {"status": "received"}, 200

def process_webhook(payload):
    # This runs asynchronously in a worker
    event_type = payload.get("eventType")
    
    if event_type == "VEHICLE_STATE":
        update_vehicle_state(payload)
    elif event_type == "VEHICLE_ERROR":
        handle_vehicle_error(payload)
Don’t do this: If you perform heavy processing before returning a response, your endpoint may timeout and Smartcar will retry, creating duplicate processing work.

Security

Always Verify Signatures

Every webhook payload includes an SC-Signature header containing an HMAC-SHA256 signature. Always verify this signature before processing the payload.
Python
import hmac
import hashlib

def verify_signature(payload, signature, management_token):
    """Verify webhook payload authenticity"""
    expected = hmac.new(
        management_token.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(expected, signature)

@app.post("/webhooks/smartcar")
def webhook_handler():
    # Get signature from header
    signature = request.headers.get('SC-Signature')
    
    # Verify before processing
    if not verify_signature(request.data, signature, MANAGEMENT_TOKEN):
        return {"error": "Invalid signature"}, 401
    
    # Safe to process
    payload = request.get_json()
    # ...
Why this matters:
  • Confirms the payload came from Smartcar
  • Prevents spoofed requests from malicious actors
  • Protects against replay attacks
See Payload Verification for complete implementation details.

Reliability

Implement Idempotency

Use the eventId field to ensure you process each event exactly once, even if Smartcar retries delivery or you reprocess from your queue.
import redis

redis_client = redis.Redis()

def process_webhook(payload):
    event_id = payload.get("eventId")
    
    # Check if already processed
    if redis_client.exists(f"processed:{event_id}"):
        print(f"Already processed {event_id}, skipping")
        return
    
    # Process the event
    process_vehicle_data(payload)
    
    # Mark as processed (expires after 7 days)
    redis_client.setex(
        f"processed:{event_id}",
        time=604800,  # 7 days
        value="1"
    )
Why this matters:
  • Smartcar retries failed deliveries with the same eventId
  • Your queue worker might process the same message multiple times
  • Prevents duplicate database updates or notifications

Handle Delivery Order

Webhook events are delivered concurrently and may arrive out of order. Don’t assume events arrive in chronological order. Use timestamps to determine freshness:
def update_vehicle_state(payload):
    vehicle_id = payload.get("vehicleId")
    delivered_at = payload["meta"]["deliveredAt"]
    
    # Get current stored state
    current = db.get_vehicle_state(vehicle_id)
    
    # Only update if this event is newer
    if current and current.updated_at > delivered_at:
        print(f"Ignoring older event for {vehicle_id}")
        return
    
    # Safe to update
    db.update_vehicle_state(vehicle_id, payload["data"], delivered_at)
Why this matters:
  • Network delays can cause events to arrive out of sequence
  • Retries of older events may arrive after newer events
  • Last-write-wins without timestamps can cause stale data

Monitoring & Observability

Log Everything

Comprehensive logging helps you debug issues, monitor performance, and understand user behavior. Key events to log:
{
  "timestamp": "2025-01-15T10:30:45Z",
  "event": "webhook.received",
  "eventId": "abc123",
  "eventType": "VEHICLE_STATE",
  "vehicleId": "def456",
  "deliveryId": "ghi789"
}
{
  "timestamp": "2025-01-15T10:30:45Z",
  "event": "signature.verified",
  "eventId": "abc123",
  "valid": true
}
{
  "timestamp": "2025-01-15T10:30:46Z",
  "event": "processing.started",
  "eventId": "abc123",
  "eventType": "VEHICLE_STATE"
}
{
  "timestamp": "2025-01-15T10:30:47Z",
  "event": "processing.failed",
  "eventId": "abc123",
  "error": "Database connection timeout",
  "stackTrace": "..."
}

Set Up Alerts

Monitor critical metrics and alert on anomalies:
  • Signature verification failures - May indicate spoofed requests
  • Processing error rate - High errors suggest code or infrastructure issues
  • Queue depth - Growing backlog indicates processing can’t keep up
  • Response time - Approaching 15s timeout threshold
  • Duplicate event processing - Idempotency checks are catching issues

Error Handling

Handle VEHICLE_ERROR Events

Don’t ignore VEHICLE_ERROR events. They indicate signal retrieval failures and require action.
def handle_vehicle_error(payload):
    vehicle_id = payload.get("vehicleId")
    errors = payload["data"].get("errors", [])
    
    for error in errors:
        signal_path = error.get("signalPath")
        error_type = error.get("type")
        message = error.get("message")
        
        if error_type == "VEHICLE_NOT_CAPABLE":
            # Vehicle doesn't support this signal
            db.mark_signal_unsupported(vehicle_id, signal_path)
            
        elif error_type == "PERMISSION_ERROR":
            # Missing permission - notify user to reconnect
            notify_user_reauth(vehicle_id)
            
        elif error_type == "UPSTREAM_ERROR":
            # Temporary OEM issue - will auto-resolve
            log_warning(f"Temporary error for {vehicle_id}: {message}")
Common error types and responses:
Error TypeMeaningAction
VEHICLE_NOT_CAPABLESignal not supported by vehicleRemove from subscription or mark as N/A
PERMISSION_ERRORMissing permission scopePrompt user to reconnect with required permissions
UPSTREAM_ERRORTemporary OEM failureLog and monitor; usually resolves automatically
RATE_LIMITToo many requestsBack off temporarily
See Event Types for complete error reference.

Graceful Degradation

Design your system to continue functioning when webhook processing fails. Example strategies:
If webhook processing fails, temporarily fall back to REST API polling for critical data.
If some signals fail, process the successful ones and mark failed signals as unavailable.
Route persistently failing events to a dead letter queue for manual investigation.
Stop processing webhooks if downstream dependencies are down, return 503 to trigger retries later.

Testing

Test in Development

Use the Smartcar Dashboard to trigger test webhook deliveries before going to production.
1

Set up local tunnel

Use ngrok or similar to expose your local server:
ngrok http 3000
2

Configure webhook

Set your ngrok URL as the callback URI in Dashboard
3

Trigger test events

Use the Dashboard to send test VEHICLE_STATE and VEHICLE_ERROR events
4

Verify handling

Check logs to confirm signature verification, queuing, and processing work correctly

Validate Edge Cases

Test your integration handles these scenarios:
  • Duplicate deliveries - Same eventId delivered multiple times
  • Out-of-order events - Older events arriving after newer ones
  • Invalid signatures - Reject payloads with wrong signature
  • Partial signal failures - Some signals succeed, others error
  • Large payloads - Maximum 50KB payload size
  • Missing fields - Gracefully handle unexpected payload structure

Performance

Optimize Response Time

Your endpoint must respond within 15 seconds. Optimize for speed:

Do

  • Return 200 immediately after persisting
  • Use in-memory queues (Redis, SQS)
  • Keep webhook handler logic minimal
  • Use database connection pooling

Don't

  • Make API calls before responding
  • Write to slow storage (S3, disk)
  • Perform complex calculations
  • Wait for downstream services

Scale Horizontally

Design for horizontal scaling to handle growing webhook volume:
  • Stateless webhook receivers - Any instance can handle any request
  • Distributed queues - SQS, RabbitMQ, Kafka for cross-instance communication
  • Load balancer health checks - Return 200 on /health endpoint
  • Auto-scaling policies - Scale based on queue depth or CPU

Next Steps