# Azure DevOps Webhooks

The Web Hooks service-hook consumer (`webHooks` / `httpRequest`) POSTs a JSON event to your URL when
something happens in an Azure DevOps project: a push, a pull request, a work item change, a build
or pipeline run. Payloads are **not signed**. Authentication is HTTP Basic: the username and
password you set on the subscription arrive on every request as
`Authorization: Basic base64(username:password)`, and your endpoint checks them.

This skill covers the `basicAuthCredentials` setting (HTTPS is required for it). The alternative
`httpHeaders` setting is not covered.

> **A static shared credential authenticates the sender, not the body, and gives no replay
> protection** (there is no signature and no timestamp). Use HTTPS, and rotate the credential if
> it leaks.

## When to Use This Skill

* How do I receive Azure DevOps webhooks / service hooks?
* How do I verify the basic authentication credentials on an Azure DevOps webhook?
* Does Azure DevOps sign webhook payloads? (No: use the shared basic auth credentials)
* How do I handle `git.push`, `git.pullrequest.created`, `workitem.updated` or `build.complete`?
* Why does my Azure DevOps webhook endpoint return 401?

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length digests, so timingSafeEqual never throws.
const safeEqual = (a, b) => crypto.timingSafeEqual(
  crypto.createHash('sha256').update(a).digest(),
  crypto.createHash('sha256').update(b).digest());

function verifyAzureDevOpsRequest(headers, user = process.env.AZURE_DEVOPS_WEBHOOK_USERNAME,
                                  pass = process.env.AZURE_DEVOPS_WEBHOOK_PASSWORD) {
  if (!user || !pass) return false;                         // never accept when unconfigured
  const m = /^basic +([A-Za-z0-9+/]+={0,2})\s*$/i.exec(headers.authorization || '');
  if (!m) return false;                                     // scheme is case-insensitive
  const decoded = Buffer.from(m[1], 'base64');
  const i = decoded.indexOf(0x3a);                          // split on the FIRST colon
  if (i < 0) return false;
  const userOk = safeEqual(decoded.subarray(0, i), Buffer.from(user));
  const passOk = safeEqual(decoded.subarray(i + 1), Buffer.from(pass));
  return userOk && passOk;                                  // evaluate both, no short-circuit
}

```

* Header: `Authorization: Basic base64(username:password)`. Env vars:
  `AZURE_DEVOPS_WEBHOOK_USERNAME` and `AZURE_DEVOPS_WEBHOOK_PASSWORD`.
* Python: `base64.b64decode(..., validate=True)`, `partition(b":")`, then `hmac.compare_digest` on
  **bytes** (encode both sides; a `str` with non-ASCII characters raises `TypeError`).
* Return `401` for a missing or malformed header or any mismatch, `400` for a body that isn't a
  single JSON object, `200` once handled.
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, and parse JSON
  only after the credential check.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/azure-devops-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/azure-devops-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/azure-devops-webhooks/examples/fastapi/).

## Event Types

Every payload has `eventType` (also called the event ID), `publisherId`, `message`,
`detailedMessage`, `resource`, `resourceVersion`, `resourceContainers` and `createdDate`, plus
`id`, and usually `subscriptionId` and `notificationId`. The handlers in the examples cover:

| `eventType` | Sent When |
| --- | --- |
| `git.push` | Code is pushed to a Git repository |
| `git.pullrequest.created` | A pull request is created |
| `git.pullrequest.updated` | A pull request is updated |
| `git.pullrequest.merged` | A pull request is merged |
| `workitem.created` | A work item is created |
| `workitem.updated` | A work item changes |
| `workitem.commented` | A work item is commented on |
| `build.complete` | A build completes |
| `ms.vss-pipelines.run-state-changed-event` | A pipeline run changes state |

Many more exist (releases, approvals, repositories, service connections, advanced security); see
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/azure-devops-webhooks/references/overview.md).

**The docs' `build.complete` and `ms.azure-devops-release.deployment-approval-pending-event` samples
are malformed JSON** (two objects concatenated with no separator). Don't copy them into fixtures or
tests verbatim; a real delivery is a single object.

## Environment Variables

```bash
AZURE_DEVOPS_WEBHOOK_USERNAME=your-webhook-username   # Service hook > Action > Basic authentication username
AZURE_DEVOPS_WEBHOOK_PASSWORD=your-webhook-password   # Service hook > Action > Basic authentication password

```

## Local Development

Azure DevOps can't reach `localhost` (webhooks can't target loopback addresses). Tunnel with the
Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 azure-devops --path /webhooks/azure-devops

```

Use the printed HTTPS URL as the subscription URL.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/azure-devops-webhooks/references/overview.md) - What Azure DevOps service hooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/azure-devops-webhooks/references/setup.md) - Create the Web Hooks subscription with basic auth
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/azure-devops-webhooks/references/verification.md) - Credential check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/azure-devops-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests