Webhooks#

Outeract uses webhooks to deliver real-time events from messaging platforms to your application.

Types of Webhooks#

Inbound Webhooks (Platform → Outeract)#

Receive messages and events from platforms (WhatsApp, Telegram, etc.). These are automatically configured when you create platform connections.

Outbound Webhooks (Outeract → Your App)#

Subscribe to events and receive them at your endpoint. Configure these to process messages in real-time.

Inbound Webhook Architecture#

Shared Webhooks#

Meta platforms (WhatsApp, Instagram, Facebook) use shared webhooks:

Platform ──► Shared Webhook URL ──► Route by Account ID ──► Connection
  • One URL for all connections of that platform
  • Routing based on business account/page ID
  • Configure once in Meta App Dashboard

Dedicated Webhooks#

Other platforms use dedicated webhooks:

Platform ──► Unique Webhook URL ──► Specific Connection
  • Each connection has its own URL
  • URL includes connection ID and secret
  • Configure in platform’s settings

Webhook URL Format#

All webhooks (both shared and dedicated) use the same URL format:

https://api.outeract.com/webhooks/{webhook_id}/{webhook_secret}

Example: https://api.outeract.com/webhooks/550e8400-e29b-41d4-a716-446655440000/a1b2c3d4e5f6

  • webhook_id: UUID identifying the webhook
  • webhook_secret: Secret token for authentication

The difference between shared and dedicated webhooks is in how incoming messages are routed internally, not in the URL format.

Webhook Verification#

Meta Platforms (WhatsApp, Instagram, Facebook)#

Meta sends a verification request when you set up webhooks:

GET /webhooks/whatsapp?hub.mode=subscribe&hub.verify_token=YOUR_TOKEN&hub.challenge=CHALLENGE_STRING

Outeract automatically responds with the challenge if the verify token matches.

Telegram#

Telegram sets webhooks via API call. Outeract provides the URL; you configure it:

curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \
  -d '{"url": "YOUR_WEBHOOK_URL"}'

Slack#

Slack sends a URL verification challenge:

{
  "type": "url_verification",
  "challenge": "challenge_string"
}

Outeract responds with the challenge to verify ownership.

Signature Verification#

All platforms use cryptographic signatures to verify webhook authenticity.

Meta Platforms (HMAC-SHA256)#

X-Hub-Signature-256: sha256=abc123...

Verification:

import hmac
import hashlib

def verify_meta_signature(body: bytes, signature_header: str, app_secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        app_secret.encode(),
        body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

Slack (HMAC-SHA256 with timestamp)#

X-Slack-Signature: v0=abc123...
X-Slack-Request-Timestamp: 1234567890

Verification:

def verify_slack_signature(body: bytes, timestamp: str, signature: str, signing_secret: str) -> bool:
    sig_basestring = f"v0:{timestamp}:{body.decode()}"
    expected = "v0=" + hmac.new(
        signing_secret.encode(),
        sig_basestring.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Twilio (HMAC-SHA1)#

X-Twilio-Signature: abc123...

Discord (Ed25519)#

X-Signature-Ed25519: abc123...
X-Signature-Timestamp: 1234567890

Webhook Payloads#

Message Received#

When a user sends a message:

{
  "type": "message.inbound",
  "data": {
    "event_id": "evt_abc123",
    "platform": "whatsapp",
    "external_message_id": "wamid.xyz",
    "message": {
      "text": "Hello!",
      "type": "text"
    },
    "sender": {
      "platform_user_id": "pu_xyz789",
      "external_id": "+14155551234",
      "name": "John Doe"
    },
    "timestamp": "2024-01-15T10:30:00Z"
  }
}

Media Message#

{
  "type": "message.inbound",
  "data": {
    "event_id": "evt_abc123",
    "platform": "whatsapp",
    "message": {
      "type": "image",
      "media": {
        "id": "file_xyz789",
        "mime_type": "image/jpeg",
        "url": "https://storage.outeract.com/files/..."
      },
      "caption": "Check this out!"
    },
    "sender": {
      "external_id": "+14155551234"
    }
  }
}

Message Status Update#

{
  "type": "message.status",
  "data": {
    "event_id": "evt_status123",
    "origin_event_id": "evt_abc123",
    "platform": "whatsapp",
    "external_message_id": "wamid.xyz",
    "status": "delivered",
    "timestamp": "2024-01-15T10:30:05Z"
  }
}

Status values:

  • sent - Message sent to platform
  • delivered - Delivered to recipient’s device
  • read - Recipient read the message
  • failed - Delivery failed

Reaction#

{
  "type": "message.reaction",
  "data": {
    "event_id": "evt_react123",
    "platform": "whatsapp",
    "reaction": {
      "emoji": "👍",
      "message_id": "wamid.xyz"
    },
    "sender": {
      "external_id": "+14155551234"
    }
  }
}

Response Requirements#

Your webhook endpoint must:

  1. Return 200-299 status within 30 seconds
  2. Return quickly - process asynchronously if needed
  3. Be idempotent - handle duplicate deliveries

Example response:

{
  "received": true
}

Or simply:

HTTP/1.1 200 OK

Retry Policy#

If your endpoint fails, Outeract retries with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours

After 5 failed attempts, the webhook is marked as failed.

Debugging Webhooks#

Webhook Logs#

View webhook delivery logs in the console:

query {
  webhookDeliveryLogs(
    webhookSubscriptionId: "ws_abc123"
    first: 20
  ) {
    edges {
      node {
        id
        status
        statusCode
        requestBody
        responseBody
        duration
        createdAt
      }
    }
  }
}

Testing Locally#

Use ngrok or webhook.site for local development:

ngrok http 3000

Then use the ngrok URL as your webhook endpoint.

Best Practices#

1. Verify Signatures Always#

Never process webhooks without verifying the signature.

2. Return Fast#

Return 200 immediately, process asynchronously:

@app.post("/webhook")
async def webhook(request: Request, background_tasks: BackgroundTasks):
    # Verify signature first
    verify_signature(request)

    # Queue for async processing
    background_tasks.add_task(process_webhook, await request.json())

    return {"received": True}

3. Handle Duplicates#

Webhooks may be delivered multiple times. Use event_id for deduplication:

processed_events = set()

def process_webhook(data):
    event_id = data["data"]["event_id"]
    if event_id in processed_events:
        return  # Already processed
    processed_events.add(event_id)
    # Process event...

4. Log Everything#

Log webhook payloads for debugging:

import logging

logger = logging.getLogger(__name__)

@app.post("/webhook")
async def webhook(request: Request):
    body = await request.body()
    logger.info(f"Webhook received: {body.decode()}")
    # Process...

5. Monitor Health#

Set up alerts for webhook failures and high latency.

Security Considerations#

  1. Always use HTTPS - Webhooks must be served over TLS
  2. Verify signatures - Reject requests with invalid signatures
  3. Validate timestamps - Reject old requests (prevent replay attacks)
  4. Use secrets - Keep webhook secrets secure
  5. Rotate secrets - Periodically rotate webhook secrets