> ## Documentation Index
> Fetch the complete documentation index at: https://moonshadow-ep3.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive Real-Time Events with Moonshadow Webhooks

> Configure Moonshadow webhooks to receive real-time event payloads to your server when integrations fire events or automations complete.

Moonshadow webhooks let you receive real-time HTTP callbacks to your server whenever events occur in your workspace. Instead of polling the API for changes, your application gets notified immediately when integrations fire events, automations complete, or other actions trigger.

## What Are Webhooks

A webhook is an HTTP POST request that Moonshadow sends to a URL you control. When a subscribed event occurs, Moonshadow packages the event data as JSON and delivers it to your endpoint. This is more efficient than polling because your server only receives data when something actually happens.

## Register a Webhook Endpoint

Create a webhook endpoint through the Moonshadow API. You need to provide a target URL and a list of event types to subscribe to.

```bash theme={null}
curl -X POST https://api.moonshadow.dev/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/moonshadow",
    "events": ["event.created", "automation.completed", "integration.connected"],
    "workspace_id": "ws_1234567890abcdef",
    "secret": "whsec_your_webhook_secret"
  }'
```

Response:

```json theme={null}
{
  "id": "whk_9876543210fedcba",
  "url": "https://your-app.com/webhooks/moonshadow",
  "events": ["event.created", "automation.completed", "integration.connected"],
  "workspace_id": "ws_1234567890abcdef",
  "status": "active",
  "created_at": "2024-01-15T10:30:00Z"
}
```

<ParamField body="url" type="string" required>
  The HTTPS endpoint where Moonshadow sends event payloads.
</ParamField>

<ParamField body="events" type="array" required>
  List of event types to subscribe to. Use `["*"]` to receive all events.
</ParamField>

<ParamField body="workspace_id" type="string" required>
  The workspace that owns this webhook.
</ParamField>

<ParamField body="secret" type="string" required>
  A secret string used to sign webhook payloads for verification.
</ParamField>

## Webhook Payload Shape

Every webhook payload has a consistent envelope. The `data` field contains the event-specific details.

```json theme={null}
{
  "id": "evt_abc123def456",
  "type": "event.created",
  "created_at": "2024-01-15T10:30:00Z",
  "workspace_id": "ws_1234567890abcdef",
  "data": {
    "integration_id": "int_abc123",
    "integration_type": "slack",
    "event_source": "slack.message.posted",
    "payload": {
      "channel": "#general",
      "user": "U1234567890",
      "text": "Hello from Slack!"
    }
  }
}
```

<ParamField body="id" type="string">
  Unique identifier for this event delivery.
</ParamField>

<ParamField body="type" type="string">
  The event type that triggered this webhook.
</ParamField>

<ParamField body="created_at" type="string">
  ISO 8601 timestamp of when the event occurred.
</ParamField>

<ParamField body="data" type="object">
  Event-specific data, including the source integration and original payload.
</ParamField>

## Verify the Signature

Moonshadow signs every webhook payload with HMAC-SHA256 using the secret you provided during registration. Verify the signature to ensure requests come from Moonshadow and have not been tampered with.

The signature is sent in the `X-Moonshadow-Signature` header as a hex string.

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhook(body, signature, secret) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(body, 'utf8')
      .digest('hex');
    
    return crypto.timingSafeEqual(
      Buffer.from(signature, 'hex'),
      Buffer.from(expected, 'hex')
    );
  }

  app.post('/webhooks/moonshadow', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-moonshadow-signature'];
    const secret = process.env.MOONSHADOW_WEBHOOK_SECRET;
    
    if (!verifyWebhook(req.body, signature, secret)) {
      return res.status(401).send('Invalid signature');
    }
    
    const event = JSON.parse(req.body);
    console.log('Received event:', event.type);
    
    res.status(200).send('OK');
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  from flask import Flask, request, abort

  app = Flask(__name__)
  WEBHOOK_SECRET = "whsec_your_webhook_secret"

  def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode('utf-8'),
          body,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)

  @app.route('/webhooks/moonshadow', methods=['POST'])
  def moonshadow_webhook():
      signature = request.headers.get('X-Moonshadow-Signature', '')
      
      if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
          abort(401)
      
      event = request.get_json()
      print(f"Received event: {event['type']}")
      
      return 'OK', 200
  ```
</CodeGroup>

<Warning>
  Always verify the signature before processing the payload. Never trust unverified webhook requests.
</Warning>

## Retries and Failure Handling

Moonshadow expects your endpoint to respond with a 2xx HTTP status code within 30 seconds. If your endpoint fails or times out, Moonshadow retries the delivery with exponential backoff.

| Attempt | Delay After Previous Attempt |
| ------- | ---------------------------- |
| 1       | Immediate                    |
| 2       | 5 seconds                    |
| 3       | 25 seconds                   |
| 4       | 2 minutes                    |
| 5       | 10 minutes                   |

After 5 failed attempts, Moonshadow marks the delivery as failed and stops retrying. You can view failed deliveries in the Events dashboard and manually retry them if needed.

Your webhook endpoint should return a 2xx response immediately after receiving the payload. Do any heavy processing asynchronously to avoid timeouts.

<Tip>
  Respond with `200 OK` as soon as you validate the signature and queue the event for processing. Handle the business logic in a background worker.
</Tip>

## Full Webhook Handler Example

Here is a complete example that receives webhooks, verifies the signature, and handles different event types:

<CodeGroup>
  ```javascript Node.js theme={null}
  const express = require('express');
  const crypto = require('crypto');

  const app = express();
  const WEBHOOK_SECRET = process.env.MOONSHADOW_WEBHOOK_SECRET;

  // Use raw body for signature verification
  app.use('/webhooks/moonshadow', express.raw({ type: 'application/json' }));

  function verifySignature(body, signature, secret) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(body, 'utf8')
      .digest('hex');
    return crypto.timingSafeEqual(
      Buffer.from(signature, 'hex'),
      Buffer.from(expected, 'hex')
    );
  }

  app.post('/webhooks/moonshadow', (req, res) => {
    const signature = req.headers['x-moonshadow-signature'];
    
    if (!signature || !verifySignature(req.body, signature, WEBHOOK_SECRET)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    const event = JSON.parse(req.body);
    
    switch (event.type) {
      case 'event.created':
        console.log('New event:', event.data.payload);
        break;
      case 'automation.completed':
        console.log('Automation finished:', event.data.automation_id);
        break;
      case 'integration.connected':
        console.log('Integration added:', event.data.integration_type);
        break;
      default:
        console.log('Unhandled event:', event.type);
    }
    
    res.status(200).send('OK');
  });

  app.listen(3000, () => console.log('Webhook server running on port 3000'));
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import os
  from flask import Flask, request, jsonify, abort

  app = Flask(__name__)
  WEBHOOK_SECRET = os.environ.get('MOONSHADOW_WEBHOOK_SECRET')

  def verify_signature(body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode('utf-8'),
          body,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)

  @app.route('/webhooks/moonshadow', methods=['POST'])
  def handle_webhook():
      signature = request.headers.get('X-Moonshadow-Signature', '')
      
      if not signature or not verify_signature(request.data, signature, WEBHOOK_SECRET):
          abort(401)
      
      event = request.get_json()
      event_type = event.get('type')
      
      if event_type == 'event.created':
          print(f"New event: {event['data']['payload']}")
      elif event_type == 'automation.completed':
          print(f"Automation finished: {event['data']['automation_id']}")
      elif event_type == 'integration.connected':
          print(f"Integration added: {event['data']['integration_type']}")
      else:
          print(f"Unhandled event: {event_type}")
      
      return jsonify({'status': 'ok'}), 200

  if __name__ == '__main__':
      app.run(port=3000)
  ```
</CodeGroup>

## Next Steps

* <Card title="Configure Notifications" icon="bell" href="/guides/notifications">
    Route events to Slack, email, or other channels with notification rules
  </Card>
* <Card title="API Rate Limits" icon="gauge" href="/reference/rate-limits">
    Understand request limits and how to handle rate limiting
  </Card>
