TempsTemps
  • Docs
  • Blog
  • Pricing
  • Enterprise
  • Security
  • Contact
Star—
TempsTemps

Open-source deployment platform with built-in error tracking, analytics, and monitoring. Runs on any VPS. No surprise bills, no data leaving your infrastructure.

  • Product
  • Features
  • Documentation
  • Changelog
  • Enterprise
  • Contact
  • Resources
  • Getting Started
  • Upgrade
  • GitHub
  • Reddit
  • Tools
  • VPS Security Scanner
  • PaaS Tax Calculator
  • Compare
  • vs Vercel
  • vs Netlify
  • vs Coolify
  • All Platforms
  • Deploy
  • Next.js
  • Node.js
  • Django
  • Laravel
  • Go
  • Rust
  • All Frameworks →
  • Legal & Compliance
  • Security & Trust
  • Data Ownership & Privacy
  • GDPR Compliance

© 2026 Temps. All rights reserved.

GitHubDocs
t
Temps

83% of Orgs Use Webhooks for CI/CD — But Most Skip HMAC Verification

83% of Orgs Use Webhooks for CI/CD — But Most Skip HMAC Verification

March 12, 2026 (4mo ago)

Temps Team

Written by Temps Team

Last updated March 12, 2026 (4mo ago)

Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

#webhooks#ci-cd#automation#hmac#devops#webhooks ci cd lifecycle
Back to all posts

Deployment webhooks are HTTP POST requests that your CI/CD platform sends to a URL you configure whenever a deployment event occurs — build started, container ready, health check passed, rollback triggered. Without them, your Slack channel stays quiet while production changes, your monitoring dashboard lags, and your team discovers outages from users rather than alerts.

This guide covers the six event types you need, how to build a secure receiver with HMAC-SHA256 verification, how to handle retries and idempotency, and how platforms like Temps fire webhooks natively at every stage of the deployment lifecycle.

TL;DR: Deployment webhooks give every system — Slack, PagerDuty, status pages, custom automations — real-time visibility into what your pipeline is doing. The three things that break webhook receivers in practice: not returning 200 fast enough, verifying signatures against parsed JSON instead of the raw body, and ignoring duplicate delivery IDs. Platforms like Temps handle the sending side with HMAC-SHA256 signatures, per-delivery logs, and manual replay — so you only need to get the receiver right.


What Are Deployment Webhooks and Why Do They Matter?

A deployment webhook fires when your infrastructure changes, not just when code changes. Git webhooks (GitHub's push and pull_request events) trigger your pipeline. Deployment webhooks report what the pipeline did — which container was started, whether health checks passed, which version was rolled back to.

The difference between polling and webhooks is the difference between refreshing your email every minute and getting a push notification. Polling wastes compute and introduces latency. Webhooks are instantaneous and cheap.

A typical deployment pipeline creates five or six events worth tracking:

Git Push → Build Start → Build Complete → Deploy Start → Health Check → Live
   ↓            ↓              ↓               ↓              ↓          ↓
webhook     webhook        webhook         webhook        webhook    webhook

Without webhooks at each stage, the only way to track deployment status is to tail logs. That works when you're the one deploying. It breaks completely when deploys run automatically on merge and stakeholders need real-time status.

What Makes Deployment Webhooks Different From Git Webhooks

Git webhooks fire when code changes. Deployment webhooks fire when infrastructure changes. They carry different information: container IDs, health check results, deployment preview URLs, rollback reasons.

You need both. Git webhooks kick off the pipeline. Deployment webhooks report back from it.


What Event Types Should Your Deployment Webhooks Cover?

A complete webhook system needs at least five event types to cover the deployment lifecycle. Here are the events that matter:

deployment.created

Fires when a new deployment is queued. This is your team's first signal that code is moving toward production.

{
  "id": "evt_a1b2c3d4",
  "event": "deployment.created",
  "timestamp": "2026-06-06T14:22:01Z",
  "deployment_id": "dep_a1b2c3d4",
  "app": "api-service",
  "environment": "production",
  "commit": {
    "sha": "f4c8e2a",
    "message": "fix: resolve timeout in payment handler",
    "author": "[email protected]",
    "branch": "main"
  },
  "triggered_by": "git_push"
}

deployment.succeeded

Fires when the container passes health checks and traffic is routed to it. This is the real "deploy is done" signal — not when the build finished, but when the new container is actually serving requests.

deployment.failed

Fires when any stage fails — build error, health check timeout, container crash. Includes the failure reason so your alerting can be specific rather than sending a generic "deploy failed" message.

deployment.ready

Fires when the deployment is fully live and all routing is updated. In zero-downtime scenarios, this fires after the old container finishes draining connections.

deployment.cancelled

Fires when a deployment is cancelled mid-flight — either manually or because a newer commit triggered a replacement deployment.

The most underused event in practice is deployment.succeeded versus the health check signal. Teams set up notifications for failure but not for the moment a new deployment becomes healthy. That gap means nobody gets a "deploy is live" confirmation unless they check the dashboard manually.


Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

How Do You Build a Webhook Receiver?

A reliable webhook receiver has three jobs: respond fast, verify the signature, handle duplicates. Getting any one of them wrong creates either security holes or operational noise.

Here's a minimal Express.js receiver that gets all three right:

const express = require('express');
const crypto = require('crypto');

const app = express();

// Important: use raw body for signature verification — JSON.parse changes the bytes
app.use('/webhooks', express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

app.post('/webhooks/deploy', async (req, res) => {
  // 1. Verify signature FIRST — before any other work
  const signature = req.headers['x-webhook-signature'];
  const timestamp = req.headers['x-webhook-timestamp'];
  const deliveryId = req.headers['x-webhook-delivery'];

  if (!verifySignature(req.body, signature, timestamp)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 2. Respond immediately — sender retries on timeout
  res.status(200).json({ received: true });

  // 3. Process asynchronously — avoid blocking the response
  const event = JSON.parse(req.body);
  event.delivery_id = deliveryId;
  handleDeployEvent(event).catch(console.error);
});

function verifySignature(payload, signature, timestamp) {
  // Reject timestamps older than 5 minutes — prevents replay attacks
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false;
  }

  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(`${timestamp}.${payload}`)
    .digest('hex');

  const expectedFull = `sha256=${expected}`;

  // Constant-time comparison — prevents timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedFull)
  );
}

async function handleDeployEvent(event) {
  switch (event.event) {
    case 'deployment.created':
      await notifySlack(`Deploying ${event.app}: ${event.commit.message}`);
      break;
    case 'deployment.succeeded':
      await notifySlack(`${event.app} is live in ${event.environment}`);
      break;
    case 'deployment.failed':
      await notifySlack(`FAILED: ${event.app} — ${event.error}`);
      await createIncident(event);
      break;
    case 'deployment.cancelled':
      await notifySlack(`Deployment cancelled: ${event.app}`);
      break;
  }
}

app.listen(3000);

Three things in that code deserve attention.

Respond Before Processing

Return 200 within a few seconds. If your receiver takes too long, the sender times out and retries — now you're processing the same event twice. Accept the payload, acknowledge receipt, then process asynchronously.

Use the Raw Body for Signatures

Express's JSON parser modifies the bytes. If you verify the HMAC against the parsed-and-re-stringified body, the signatures won't match. Always capture the raw bytes before parsing. The express.raw({ type: 'application/json' }) middleware handles this.

Timing-Safe Comparison

Using === to compare signature strings leaks information through timing differences. crypto.timingSafeEqual compares in constant time, eliminating the timing side channel. This is a real attack vector, not a theoretical one.


How Do You Verify Webhook Signatures With HMAC-SHA256?

HMAC-SHA256 is the standard method for authenticating webhook payloads. Without signature verification, anyone who discovers your webhook URL can send forged events — triggering false alerts, fake rollbacks, or bogus incident records.

The verification flow works in four steps:

Step 1: Share a Secret

When you register a webhook endpoint, the platform generates a shared secret. Both sides know it. It never appears in the webhook payload itself. Store it as an environment variable — never commit it to source control.

Step 2: Sign the Payload

The sender creates an HMAC-SHA256 hash of a message composed of the timestamp and request body, separated by a period: {timestamp}.{body}. The signature is sent in an HTTP header alongside the timestamp:

X-Webhook-Signature: sha256=a1b2c3d4e5f6...
X-Webhook-Timestamp: 1749254521
X-Webhook-Delivery: 42
X-Webhook-Event: deployment.succeeded

Step 3: Verify on Receipt

Your receiver computes the same HMAC using the shared secret and the raw request body. If the computed signature matches the header, the payload is authentic and hasn't been tampered with:

function verifyWebhookSignature(secret, payload, signature, timestamp) {
  const signedContent = `${timestamp}.${payload}`;

  const computed = crypto
    .createHmac('sha256', secret)
    .update(signedContent)
    .digest('hex');

  const computedFull = `sha256=${computed}`;

  return crypto.timingSafeEqual(
    Buffer.from(computedFull, 'utf8'),
    Buffer.from(signature, 'utf8')
  );
}

Step 4: Reject Stale Timestamps

Reject webhooks with timestamps older than 5 minutes. This prevents replay attacks where an attacker captures a legitimate webhook and resends it later. A 5-minute window accounts for clock drift and network latency while keeping the replay window small.


Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

How Do You Handle Retries and Idempotency?

Networks fail. Receivers crash. Timeouts happen. A robust webhook system retries failed deliveries with exponential backoff. Your receiver will occasionally receive the same event twice — whether due to retries or a brief network partition. It must handle duplicates without creating duplicate Slack messages, duplicate incidents, or duplicate database rows.

Exponential Backoff With Jitter

Retries should follow an exponential backoff pattern. The first retry fires after a few seconds, subsequent retries after longer intervals. Adding random jitter prevents thundering herd problems when many webhooks fail simultaneously.

A typical retry schedule looks like:

Attempt 1: immediate
Attempt 2: 10 seconds + random(0-5s)
Attempt 3: 30 seconds + random(0-10s)
Attempt 4: 2 minutes + random(0-30s)
Attempt 5: 10 minutes + random(0-60s)

Most platforms retry 3–6 times over a few hours before marking a delivery as failed.

Making Your Receiver Idempotent

Every webhook payload includes a unique delivery ID — from Temps this comes in the X-Webhook-Delivery header. Store these IDs and check for duplicates before processing:

const processedEvents = new Set(); // Use Redis with TTL in production

async function handleWebhook(event) {
  if (processedEvents.has(event.delivery_id)) {
    console.log(`Duplicate delivery ${event.delivery_id}, skipping`);
    return;
  }

  processedEvents.add(event.delivery_id);
  await routeEvent(event);
}

In production, use Redis with a 48-hour TTL instead of an in-memory Set. In-memory state doesn't survive restarts.

What Counts as a Successful Delivery?

The sender considers a webhook delivered when it receives an HTTP 2xx response. A 4xx, 5xx, timeout, or connection refused all trigger retries. Some status codes carry special meaning: a 410 Gone response tells most webhook senders to permanently deactivate the endpoint. Don't return 410 by accident — a simple 500 is the right error code if your receiver is temporarily unhealthy.


How Do You Send Deployment Webhooks to Slack and Discord?

Slack and Discord are the two most common webhook destinations for deployment notifications. Setting up deployment notifications for either takes under five minutes once your receiver is wired up.

Slack Integration

Format deployment events into Slack's Block Kit structure:

async function notifySlack(event, webhookUrl) {
  const color = event.event === 'deployment.succeeded' ? '#36a64f'
    : event.event === 'deployment.failed' ? '#ff0000'
    : '#3498db';

  const payload = {
    attachments: [{
      color,
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*${event.event.replace('.', ' ').toUpperCase()}*\n` +
                  `App: \`${event.app}\`\n` +
                  `Environment: ${event.environment}\n` +
                  `Commit: ${event.commit?.message || 'N/A'}`
          }
        },
        {
          type: 'context',
          elements: [{
            type: 'mrkdwn',
            text: `Triggered by ${event.triggered_by} at ${event.timestamp}`
          }]
        }
      ]
    }]
  };

  await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });
}

Discord Integration

Discord uses embeds instead of blocks:

async function notifyDiscord(event, webhookUrl) {
  const color = event.event === 'deployment.succeeded' ? 0x36a64f
    : event.event === 'deployment.failed' ? 0xff0000
    : 0x3498db;

  const payload = {
    embeds: [{
      title: event.event.replace('.', ' ').toUpperCase(),
      color,
      fields: [
        { name: 'App', value: event.app, inline: true },
        { name: 'Environment', value: event.environment, inline: true },
        { name: 'Commit', value: event.commit?.message || 'N/A' }
      ],
      timestamp: event.timestamp
    }]
  };

  await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });
}

Routing Events to the Right Channel

Not every event needs the same audience. Build events go to #dev-deploys. Failures go to #incidents. Your webhook receiver becomes a router:

const CHANNELS = {
  'deployment.created':   process.env.SLACK_DEV_CHANNEL,
  'deployment.succeeded': process.env.SLACK_DEV_CHANNEL,
  'deployment.ready':     process.env.SLACK_DEV_CHANNEL,
  'deployment.failed':    process.env.SLACK_INCIDENTS_CHANNEL,
  'deployment.cancelled': process.env.SLACK_DEV_CHANNEL,
};

How Does Temps Handle Deployment Webhooks?

Temps is a self-hosted deployment platform (Apache 2.0) that replaces Vercel, PostHog, FullStory, Sentry, Pingdom, managed databases, and transactional email in a single Rust binary. Temps Cloud runs on Hetzner at cost plus 30% — roughly $6/month — with no per-seat fees and no bandwidth bills.

Temps fires webhooks at every stage of the deployment lifecycle. Because Temps controls the full pipeline from git push through health check, it has visibility into events that external CI systems can't observe.

Real Event Types

Temps supports these deployment webhook event types out of the box:

  • deployment.created — deployment queued
  • deployment.succeeded — container healthy, traffic routed
  • deployment.failed — any stage failed with reason
  • deployment.cancelled — deployment stopped before completion
  • deployment.ready — fully live, connections drained

Plus project events (project.created, project.deleted), domain events (domain.created, domain.provisioned), and email tracking events (email.delivered, email.bounced, email.complained).

Built-In HMAC-SHA256 Signing

Every webhook Temps sends includes four headers:

X-Webhook-Signature: sha256=<hmac-sha256-of-timestamp.body>
X-Webhook-Timestamp: <unix-timestamp>
X-Webhook-Delivery: <delivery-id>
X-Webhook-Event: deployment.succeeded

The signing format matches the pattern shown earlier: HMAC-SHA256(secret, "{timestamp}.{body}"). The result is prefixed with sha256= so you can verify the algorithm at a glance.

Secrets are encrypted at rest using Temps's built-in encryption service. Webhook deliveries use a 30-second timeout and disable redirect following to prevent SSRF via redirect chains.

Automatic Retries With Delivery Logs

Failed deliveries retry automatically. Every webhook delivery is logged with the full request payload, response status code, and delivery latency. When a receiver returns 500, you can see the exact payload that was sent and replay it from the Temps dashboard without re-registering the endpoint.

Registering a Webhook

# Register a webhook endpoint via the Temps CLI
bunx @temps-sdk/cli webhooks create \
  --project-id <your-project-id> \
  --url https://your-app.com/webhooks/deploy \
  --events deployment.created,deployment.succeeded,deployment.failed \
  --secret whsec_your_signing_secret

Or via the REST API:

curl -X POST https://your-temps-instance.com/api/projects/{project_id}/webhooks \
  -H "Authorization: Bearer $TEMPS_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/deploy",
    "events": ["deployment.created", "deployment.succeeded", "deployment.failed", "deployment.cancelled", "deployment.ready"],
    "secret": "whsec_your_signing_secret",
    "enabled": true
  }'

Temps vs. GitHub Actions Webhooks vs. Self-Built Webhook Middleware

FeatureTempsGitHub Actions + WebhooksCustom middleware
Event coverageAll deployment lifecycle eventsGit events onlyWhatever you build
HMAC signingBuilt-in, encrypted secretsConfigurableBuild it yourself
Delivery logsPer-delivery, with replayNoneBuild it yourself
Retry logicAutomatic exponential backoffNoneBuild it yourself
Health check eventsYes (deployment.ready)NoPossible with polling
Timeout30 secondsN/AConfigurable
Setup time~5 minutesHours of glue codeDays
CostSelf-host free, Cloud ~$6/moSee GitHub pricing pageInfrastructure cost

GitHub Actions knows when a workflow succeeds but doesn't know whether the deployed container passed its health check. Temps runs the health check and fires the webhook when the container is actually serving traffic — not just when the deploy command exited.


Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

FAQ

What webhook event types does Temps support?

Temps supports five deployment lifecycle events (deployment.created, deployment.succeeded, deployment.failed, deployment.cancelled, deployment.ready), two project events, two domain events, and three email tracking events. You subscribe to specific event types when registering a webhook endpoint.

How many webhook endpoints can I register per project?

Temps allows up to 10 webhook endpoints per application, each subscribing to different event types. If you need to fan out to more systems, route through a single aggregator receiver that dispatches to multiple destinations — this also centralizes your signature verification and delivery logging.

What happens if my webhook receiver is down?

Temps retries with exponential backoff. If all retries fail, the delivery is marked as failed in the dashboard. You can manually replay failed deliveries once your receiver is healthy again without re-triggering the original deployment event. Design your receiver to be idempotent — process the same delivery ID only once — so replayed events don't create duplicate alerts.

Can I use webhooks to trigger automatic rollbacks?

Yes, but be careful with false positives. Build a receiver that watches for deployment.succeeded events, runs smoke tests against the new deployment URL, and calls the Temps rollback API if the tests fail. A slow response during a cold start could trigger a rollback of a healthy container. Start with alerting and graduate to automated rollbacks once you trust your smoke tests.

How do I test webhooks during local development?

Use a tunneling tool like ngrok or Cloudflare Tunnel to expose your local receiver to the public internet. Register the tunnel URL in your Temps webhook configuration, push a commit, and watch the events arrive. For unit testing, capture example payloads from Temps's delivery logs and replay them with curl. Always test signature verification separately against raw body bytes — it's the part most likely to break when you change how you parse the request.

How does Temps sign webhooks?

Temps uses HMAC-SHA256 with the message format {unix-timestamp}.{raw-body}. The result appears in the X-Webhook-Signature header as sha256={hex-encoded-hash}. The timestamp is sent separately in X-Webhook-Timestamp. Verify the timestamp is within 5 minutes of your current time before computing the HMAC to prevent replay attacks.