> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crewship.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Trigger runs and receive notifications via webhooks

## Overview

Crewship webhooks let you:

* **Incoming webhooks**: Trigger runs via HTTP POST requests from external systems
* **Outgoing webhooks**: Receive notifications when runs complete

## Incoming Webhooks

Incoming webhooks provide unique URLs that trigger runs when called. Use them to integrate with:

* CI/CD pipelines (GitHub Actions, GitLab CI, etc.)
* Scheduling services (cron jobs, cloud schedulers)
* No-code automation tools (Zapier, Make, n8n)
* Custom integrations

### Creating an Incoming Webhook

1. Go to your deployment in the [Console](https://console.crewship.dev)
2. Click **Webhooks**
3. Click **Add Incoming Webhook**
4. Enter a name and select the environment (production/staging)
5. Copy the generated webhook URL

### Triggering a Run

Send a POST request to the webhook URL with your run input:

```bash theme={null}
curl -X POST "https://api.crewship.dev/webhooks/runs/YOUR_WEBHOOK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"topic": "AI agents", "year": "2025"}'
```

**Response (202 Accepted):**

```json theme={null}
{
  "run_id": "run_abc123",
  "version_id": "ver_xyz789",
  "version_number": 5,
  "status": "running"
}
```

The webhook waits for the run to start before returning, so a `202` response means the run is actually executing.

### Example: GitHub Actions

Trigger a crew run on every push to main:

```yaml theme={null}
name: Run Crew
on:
  push:
    branches: [main]

jobs:
  run-crew:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger Crewship Run
        run: |
          curl -X POST "${{ secrets.CREWSHIP_WEBHOOK_URL }}" \
            -H "Content-Type: application/json" \
            -d '{"commit": "${{ github.sha }}", "branch": "${{ github.ref_name }}"}'
```

### Example: Scheduled Runs

Use a cron service or cloud scheduler to trigger runs on a schedule:

```bash theme={null}
# Daily at 9am UTC via cron
0 9 * * * curl -X POST "https://api.crewship.dev/webhooks/runs/YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"date": "$(date +%Y-%m-%d)"}'
```

## Outgoing Webhooks

Outgoing webhooks notify external systems when runs complete. Use them to:

* Send Slack/Discord notifications
* Update databases or dashboards
* Trigger downstream workflows
* Log results to external systems

### Creating an Outgoing Webhook

1. Go to your deployment in the [Console](https://console.crewship.dev)
2. Click **Webhooks**
3. Click **Add Outgoing Webhook**
4. Enter a name, target URL, and select events
5. Save the signing secret securely

### Events

| Event           | Description                |
| --------------- | -------------------------- |
| `run.succeeded` | Run completed successfully |
| `run.failed`    | Run failed with an error   |

### Webhook Payload

When a run completes, Crewship sends a POST request to your URL:

```json theme={null}
{
  "event": "run.succeeded",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "run_id": "run_abc123",
    "deployment_id": "dep_xyz789",
    "status": "succeeded",
    "output": {
      "result": "Your crew output here..."
    },
    "started_at": "2025-01-15T10:29:15.000Z",
    "completed_at": "2025-01-15T10:30:00.000Z"
  }
}
```

### Security Headers

Every outgoing webhook request includes these headers:

| Header                 | Description                            |
| ---------------------- | -------------------------------------- |
| `X-Crewship-Event`     | Event type (e.g., `run.succeeded`)     |
| `X-Crewship-Delivery`  | Unique delivery ID for debugging       |
| `X-Crewship-Timestamp` | Unix timestamp when request was signed |
| `X-Crewship-Signature` | HMAC-SHA256 signature for verification |

### Verifying Signatures

Always verify webhook signatures to ensure requests are from Crewship:

```typescript theme={null}
import crypto from 'crypto'

function verifyWebhookSignature(
  secret: string,
  timestamp: string,
  body: string,
  signature: string
): boolean {
  const signedPayload = `${timestamp}.${body}`
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex')

  return signature === `sha256=${expectedSignature}`
}

// In your webhook handler
app.post('/webhook', (req, res) => {
  const timestamp = req.headers['x-crewship-timestamp']
  const signature = req.headers['x-crewship-signature']
  const body = JSON.stringify(req.body)

  if (!verifyWebhookSignature(WEBHOOK_SECRET, timestamp, body, signature)) {
    return res.status(401).send('Invalid signature')
  }

  // Process the webhook...
  const { event, data } = req.body
  console.log(`Run ${data.run_id} ${event}`)

  res.status(200).send('OK')
})
```

### Python Example

```python theme={null}
import hmac
import hashlib

def verify_webhook_signature(secret: str, timestamp: str, body: str, signature: str) -> bool:
    signed_payload = f"{timestamp}.{body}"
    expected = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return signature == f"sha256={expected}"

# In your Flask handler
@app.route('/webhook', methods=['POST'])
def webhook():
    timestamp = request.headers.get('X-Crewship-Timestamp')
    signature = request.headers.get('X-Crewship-Signature')
    body = request.get_data(as_text=True)

    if not verify_webhook_signature(WEBHOOK_SECRET, timestamp, body, signature):
        return 'Invalid signature', 401

    data = request.json
    print(f"Run {data['data']['run_id']} {data['event']}")

    return 'OK', 200
```

### Delivery Logs

View delivery attempts in the Console:

1. Go to your deployment → **Webhooks**
2. Find your outgoing webhook
3. Click **Deliveries**

Each delivery shows:

* Status (succeeded/failed)
* HTTP response code
* Error message (if failed)
* Timestamp

## Managing Webhooks

### Regenerating Secrets

If your webhook secret is compromised:

1. Go to your webhook in the Console
2. Click **Regenerate Secret**
3. Update your integration with the new secret

<Warning>
  The old secret stops working immediately. Update your integration before regenerating.
</Warning>

### Disabling Webhooks

Toggle the **Enabled** switch to temporarily disable a webhook without deleting it.

### Environment Filtering

Webhooks are scoped to an environment (production or staging). Create separate webhooks if you need different behavior per environment.

## Best Practices

<AccordionGroup>
  <Accordion title="Always verify signatures">
    Never process outgoing webhooks without verifying the signature. This prevents attackers from sending fake webhook payloads.
  </Accordion>

  <Accordion title="Respond quickly">
    Return a 2xx response within 30 seconds. Crewship waits for your response before marking the delivery as complete.
  </Accordion>

  <Accordion title="Handle duplicates">
    Use the `X-Crewship-Delivery` header to deduplicate. In rare cases, the same event may be delivered twice.
  </Accordion>

  <Accordion title="Keep secrets secure">
    Store webhook secrets in environment variables or a secrets manager. Never commit them to version control.
  </Accordion>
</AccordionGroup>

## Testing Webhooks

Use [webhook.site](https://webhook.site) to test outgoing webhooks:

1. Go to webhook.site and copy your unique URL
2. Create an outgoing webhook with that URL
3. Trigger a run
4. View the payload on webhook.site

## Related

<CardGroup cols={2}>
  <Card title="Streaming" icon="signal-stream" href="/guides/streaming">
    Real-time event streaming via SSE
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/runs/create">
    Create runs via API
  </Card>
</CardGroup>
