Agent skill
Azure DevOps Webhooks Skill
Receive and authenticate Azure DevOps webhooks (Web Hooks service hooks). Use when setting up an Azure DevOps service hook endpoint, verifying the basic authentication credentials, debugging 401s, or handling events like git.push, git.pullrequest.created, git.pullrequest.merged, workitem.created, workitem.updated and build.complete.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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.updatedorbuild.complete? - Why does my Azure DevOps webhook endpoint return 401?
Verification (core)
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_andDEVOPS_ WEBHOOK_ USERNAME AZURE_.DEVOPS_ WEBHOOK_ PASSWORD - Python:
base64.b64decode(..., validate=True),partition(b":"), thenhmac.compare_digeston bytes (encode both sides; astrwith non-ASCII characters raisesTypeError). - Return
401for a missing or malformed header or any mismatch,400for a body that isn't a single JSON object,200once 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/, examples/nextjs/, 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.
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
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):
npx hookdeck-cli listen 3000 azure-devops --path /webhooks/azure-devops
Use the printed HTTPS URL as the subscription URL.
Resources
- references/overview.md - What Azure DevOps service hooks are, events and payload shape
- references/setup.md - Create the Web Hooks subscription with basic auth
- references/verification.md - Credential check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests