# Webhook ingest

Many services already speak HTTPS webhooks. Azure DevOps Service Hooks, Azure Event Grid system topics, GitHub push webhooks — they POST a JSON body to a URL when something happens. ActivityLog accepts these natively.

The endpoint is:

```
POST https://api.activitylog.com/api/v1/webhooks/{alw_TOKEN}
```

where `{alw_TOKEN}` is a **webhook URL token** that you mint in the portal (note the `alw_` prefix vs `al_` for native ingest).

## Mint a webhook URL token

1. Sidebar → **Systems** → **New System** (or pick an existing one for webhook ingest).
2. On the system detail page click **New Token**.
3. In the dialog, check **URL token** (so it generates an `alw_…` value instead of `al_…`).
4. Pick a retention.
5. **Generate**, then copy the token.

The full URL to give your source system is:

```
https://api.activitylog.com/api/v1/webhooks/alw_REPLACE_WITH_YOUR_TOKEN
```

> Webhook URL tokens are URL-safe — your source system pastes them as part of the URL, not as a header. The token still gives only ingest access; loss has the same blast radius as a regular `al_` token.

![Token mint dialog with URL-token toggle](screenshots/portal-systems-token-mint-urltoken.png)

## What the endpoint accepts

Three shapes, picked by request body, all on the same URL:

| Source | Body shape | Detected by |
|---|---|---|
| **Azure Event Grid** validation handshake | JSON array, first element has `eventType: "Microsoft.EventGrid.SubscriptionValidationEvent"` | body content |
| **CloudEvents v1.0** preflight | `OPTIONS` request with `WebHook-Request-Origin` header | HTTP method |
| **Anything else** — single object or array | JSON object or JSON array | body content |

You don't need to tell us which is which — the dispatcher figures it out.

## Azure Event Grid

### Subscription validation (one-time, automatic)

When Event Grid first registers a subscription against your webhook URL, it POSTs a validation envelope. Our endpoint detects and answers it transparently — no work on your end. No `Message` row is created for the validation event.

### Steady-state events

After validation, Event Grid POSTs event batches:

```http
POST /api/v1/webhooks/alw_REPLACE_WITH_YOUR_TOKEN
Content-Type: application/json
aeg-event-type: Notification

[
  { "id": "...", "eventType": "Microsoft.Resources.ResourceWriteSuccess", ... },
  { "id": "...", "eventType": "Microsoft.Resources.ResourceDeleteSuccess", ... }
]
```

Response (HTTP 202 Accepted):

```json
{ "messageIds": ["mid_...", "mid_..."] }
```

Each event becomes one `Message` row. The Azure resource id, operation name, actor, and full event payload are stored (subject to your tier — see [Free-tier auto-projection](#free-tier-auto-projection) below).

### Provision an Event Grid system topic

Walk through this in your Azure portal:

1. **Event Grid → System Topics → Create**.
2. **Topic type**: pick `Microsoft.Resources.Subscriptions` for subscription-level CUD, or any other system topic you want to forward.
3. **Source**: the Azure subscription you want events from.
4. **Resource group + name**: any.
5. After creation, open the topic and **+ Event Subscription**.
6. **Endpoint type**: Webhook.
7. **Endpoint**: paste `https://api.activitylog.com/api/v1/webhooks/alw_REPLACE_WITH_YOUR_TOKEN`.
8. **Create**.

Event Grid sends the validation event, our endpoint echoes the validation code, and you're live.

For the full deep-dive on Event Grid handshake semantics and the data model, see [`../Event-Grid-Ingest.md`](../Event-Grid-Ingest.md).

## Azure DevOps Service Hooks

In your ADO organization:

1. **Project Settings → Service hooks → + Create subscription**.
2. **Service**: Web Hooks.
3. **Trigger**: pick an event type (`Code pushed`, `Pull request created`, `Work item updated`, `Build completed`, etc.).
4. **Action**:
   - **URL**: `https://api.activitylog.com/api/v1/webhooks/alw_REPLACE_WITH_YOUR_TOKEN`
   - **Basic authentication**: leave blank.
   - **Resource details to send**: All.
   - **Messages to send**: All.
   - **Detailed messages to send**: All.
5. **Test** — should respond with HTTP 201/202.
6. **Finish**.

Each ADO event becomes one `Message` row tagged with the event type (`git.push`, `git.pullrequest.created`, `workitem.updated`, etc.).

Repeat per event type you want to capture; ADO requires one subscription per event class.

See [`71-integrations-azure-devops.md`](./71-integrations-azure-devops.md) for the recommended event-type set and a complete walkthrough.

## GitHub webhooks

In your GitHub repository:

1. **Settings → Webhooks → Add webhook**.
2. **Payload URL**: `https://api.activitylog.com/api/v1/webhooks/alw_REPLACE_WITH_YOUR_TOKEN`
3. **Content type**: `application/json`.
4. **Secret**: leave blank (we don't validate HMAC today — coming).
5. **Which events**: pick what you want. `Push events` + `Pull request` + `Workflow runs` is a sensible starter set.
6. **Active**: ✓.
7. **Add webhook**.

GitHub pings the URL on save. Our endpoint accepts the ping as a no-op success.

## Free-tier auto-projection

Webhook payloads can be large (ADO PRs often ship 18 KB+ of curated JSON). On the Free tier, the API automatically projects each webhook event down to a lean ~1 KB metadata-only envelope:

- `event_type` — the source event type
- `actor_id` / `actor_email` — who did it
- `target_id` / `target_url` — what it acted on
- `summary` — first 200 chars of the source's summary field

The full payload is discarded. This is the **only way** the Free tier accepts webhook ingest — without projection, a single ADO event would blow the 16 KB body cap.

Pro and Enterprise tenants keep the full source-curated payload (Mode 2 storage — see [`../../../ActivityLog-Integrations/Common-Architecture.md` § 4](../../../ActivityLog-Integrations/Common-Architecture.md)).

> **Upgrade impact:** When you upgrade Free → Pro, **new** webhook events arrive with full payloads. Existing rows stay at lean projection — no backfill.

## Response codes

| HTTP | Meaning |
|---|---|
| 200 | Validation handshake (Event Grid / CloudEvents preflight) acknowledged |
| 201 | Single event stored — body is `{ "id": "mid_...", "ingestedAt": "..." }` |
| 202 | Batch stored — body is `{ "messageIds": ["mid_...", ...] }` |
| 401 | (rare — usually 404 below) |
| 404 | The URL token is wrong / revoked / never existed |
| 413 | Payload too large (no enforced cap today; reserved for future per-tier limits) |
| 429 | Rate-limit hit |
| 500 | Server fault |

> The webhook endpoint deliberately returns **404 `webhook-not-found`** (not 401) on unknown tokens — this is the existing behavior and a few existing integrations depend on it. Don't probe-test tokens against the webhook endpoint expecting 401.

## Best practices

- **One system + token per source class.** Azure events go to one system, ADO to another, GitHub to a third. Independent rotation, cleaner filtering.
- **Pick a retention that matches the source.** ADO/GitHub: 1825 days (5y) — these are activity-history. Event Grid: same. Short retention here doesn't save much (metadata is cheap) and loses audit value.
- **Test with `Test connection`** (most webhook UIs have this) before you wire up production. A successful test puts an event in the portal Messages view within seconds.
- **Don't share the URL.** Treat it like a bearer token — anyone with the URL can ingest as that source.

## What's next

| Goal | Doc |
|---|---|
| Full Event Grid provisioning details | [`../Event-Grid-Ingest.md`](../Event-Grid-Ingest.md) |
| ADO-specific event-type catalog and walkthrough | [`71-integrations-azure-devops.md`](./71-integrations-azure-devops.md) |
| Webhook-vs-pull tradeoffs for M365 | [`70-integrations-m365.md`](./70-integrations-m365.md) |

## Troubleshooting

**Event Grid says "Subscription validation failed" when I create the subscription.**
The URL is wrong, the token is revoked, or the system is suspended. Test from `curl`:
```bash
curl -X POST "https://api.activitylog.com/api/v1/webhooks/alw_REPLACE_WITH_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[{"eventType":"Microsoft.EventGrid.SubscriptionValidationEvent","data":{"validationCode":"test123"}}]'
```
Should return `{"validationResponse":"test123"}`. If not, check the system's status in the portal.

**ADO test says success but no message appears.**
Check the response code in ADO's Service Hook history (Project Settings → Service hooks → History tab). 201/202 = success; anything else, the event didn't store. Most common cause: ADO is configured with a stale webhook URL after a token rotation.

**I want HMAC signature validation on incoming events.**
Not implemented today — both ADO and Event Grid system topics don't compute one by default. Tracked as a future hardening item; if you need it now, run an Azure Function relay that validates and re-forwards.
