# Webhook Service

# Webhook Service (Port 8007) {#webhook-service-port-8007}


The **webhook-service** manages webhook subscriptions, delivers events to external URLs, and handles retry logic.


## Database {#database}


**Database:** `webhook_db`

| Table | Purpose |
|-------|---------|
| `webhook_subscriptions` | Webhook endpoint configurations |
| `webhook_deliveries` | Delivery attempt records |


## API Endpoints {#api-endpoints}


| Method | Path | Purpose |
|--------|------|---------|
| GET | `/webhooks` | List webhooks |
| POST | `/webhooks` | Create webhook |
| DELETE | `/webhooks/{id}` | Delete webhook |
| POST | `/webhooks/{id}/test` | Test webhook delivery |
| GET | `/webhooks/{id}/deliveries` | List delivery history |


## Event Consumption {#event-consumption}


The webhook-service subscribes to the Redis `webhook:events` channel and processes events asynchronously:

```python
@celery_app.task
async def process_webhook_event(event_data: dict):
    webhooks = await webhook_repo.find_by_event_type(
        event_data["event_type"],
        event_data["tenant_id"]
    )
    for webhook in webhooks:
        await deliver_webhook(webhook, event_data)
```


## Retry Logic {#retry-logic}


Failed deliveries are retried with exponential backoff:

```python
RETRY_SCHEDULE = [60, 300, 900, 3600, 21600]  # 1m, 5m, 15m, 1h, 6h

async def deliver_webhook(webhook, event):
    for attempt, delay in enumerate(RETRY_SCHEDULE):
        try:
            async with httpx.AsyncClient() as client:
                resp = await client.post(webhook.url, json=event, timeout=30)
            if resp.status_code < 500:
                return  # Success
        except Exception:
            pass
        await asyncio.sleep(delay)
```

The `webhook-beat` Celery Beat container retries pending webhooks every 1 minute.


## Supported Events {#supported-events}


The service consumes events from survey-service and data-service:

- `survey.created`, `survey.updated`, `survey.deleted`, `survey.published`, `survey.unpublished`
- `response.submitted`, `response.deleted`, `response.invalidated`
