AtlasWork, planned itself.

The AI-native, all-in-one work platform. Tasks, projects, CRM, contracts, and analytics in one calm workspace.

All systems operational
  • SOC 2 II
  • ISO 27001
  • HIPAA
  • GDPR

Product

  • Overview
  • PDF tools
  • Diagram tools
  • People & HR
  • Integrations
  • Marketplace
  • Pricing

Resources

  • Guides
  • Glossary
  • Compare
  • Docs
  • API reference
  • Support
  • Changelog
  • Status

Company

  • About
  • Careers
  • Press
  • Contact

Legal & trust

  • Trust center
  • Security
  • Privacy
  • Terms
  • DPA
  • GDPR
  • SLA
  • Refunds
  • Google API data
Atlas, a product by wrxstack.com·© 2026 wrxstack·All rights reserved
PrivacyTermsSecurityStatus
Skip to documentation
Docs
Back to Atlas

Start here

  • Overview

Developer

  • REST API guide
  • Authentication
  • API reference
  • MCP (AI agents)
  • SDKs
  • Quick actions

Webhooks

  • Overview
  • Quickstart
  • Events
  • Payloads and headers
  • Security and signing
  • Delivery and retries
  • Managing via API

Connect

  • Connectors
  • Integrations

Product

  • Collaboration and chat

Reference

  • Glossary
  • Keyboard shortcuts
  • Module reference

Webhooks

Security and signing

Every Atlas delivery is signed with an HMAC-SHA256 over its timestamp, id, and raw body. Verify that signature, reject replays, and rotate keys without dropping a delivery.

Verify every signature

Atlas signs every delivery and sends the signature in the x-atlas-webhook-signature header. The value is a lowercase hex HMAC-SHA256 digest. The key is your webhook signing secret. The message is three parts joined by literal dots: `${timestamp}.${webhookId}.${body}`.

  • timestamp is the exact string value of the x-atlas-webhook-timestamp header, a count of unix milliseconds.
  • webhookId is the exact value of the x-atlas-webhook-id header.
  • body is the exact raw request body, byte for byte. Never re-serialize parsed JSON before signing: whitespace and key order differences will change the digest and every verification will fail.

To verify a delivery, recompute that HMAC with your signing secret and compare it to the x-atlas-webhook-signature header using a constant-time comparison. A plain === or == comparison leaks timing information an attacker can use to forge a signature, so always use the constant-time helper your language provides. Reject the delivery on any mismatch. Each snippet below also rejects a delivery whose timestamp is more than five minutes from your current time.

import crypto from "node:crypto";
import express from "express";

const SIGNING_SECRET = process.env.ATLAS_WEBHOOK_SECRET;
const app = express();

// Read the raw body: the signature is over the exact bytes Atlas sent.
// Do NOT verify against re-serialized parsed JSON.
app.post("/atlas/webhook", express.raw({ type: "*/*" }), (req, res) => {
  const timestamp = req.header("x-atlas-webhook-timestamp"); // unix ms string
  const webhookId = req.header("x-atlas-webhook-id");
  const signature = req.header("x-atlas-webhook-signature"); // lowercase hex
  const body = req.body.toString("utf8"); // raw string

  if (!timestamp || !webhookId || !signature) {
    return res.status(400).send("missing signature headers");
  }

  // Reject stale or future-dated deliveries (+/- 5 minutes).
  const skew = Math.abs(Date.now() - Number(timestamp));
  if (!Number.isFinite(skew) || skew > 300000) {
    return res.status(400).send("timestamp outside tolerance");
  }

  const message = timestamp + "." + webhookId + "." + body;
  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(message)
    .digest("hex");

  // Constant-time compare. timingSafeEqual throws on length mismatch,
  // so guard the length first.
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signature, "utf8");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send("invalid signature");
  }

  // Signature is valid. Enqueue work and acknowledge fast.
  res.status(200).send("ok");
});

Confirm your implementation matches ours. Paste your signing secret and a sample delivery below to compute the exact x-atlas-webhook-signature Atlas would send. It runs entirely in your browser, so the secret never leaves this page.

Signature playgroundRuns in your browser only
Computed signature

Enter a secret to compute the signature.

signed message: 1783412096000.whd_5a1b2c3d.{"id":"evt_9f2c","tenantId":"ten_123","action":"task.completed","target":"task:t_456","actorId":"usr_789","actorKind":"user","context":{},"at":"2026-07-07T12:34:56.000Z"}


Replay protection

A valid signature proves a payload came from Atlas, but on its own it does not stop an attacker who captured a genuine delivery from replaying it later. Your receiver MUST enforce two additional checks so a captured request cannot be re-sent.

The first check is a timestamp tolerance. Reject the delivery if the x-atlas-webhook-timestamp is more than five minutes (300000 ms) away from your current time, in either direction. Because the timestamp is part of the signed message, an attacker cannot edit it to look fresh without breaking the signature. This bounds how long a captured request stays useful to five minutes.

The second check closes that five-minute gap. Treat the x-atlas-webhook-id as a single-use idempotency key: record every id you accept and reject any id you have already seen within the last ten minutes. Storing seen ids for ten minutes safely covers the five-minute timestamp window on both sides. The two checks reinforce each other: the timestamp tolerance keeps your set of seen ids small and bounded, and the id check stops a replay that arrives inside the tolerance window.

Both checks are required

Reject any delivery whose timestamp is more than five minutes from now, and reject any x-atlas-webhook-id you have already processed in the last ten minutes. A valid signature alone does not defend against replay: enforce both.

Key rotation

Rotate a webhook signing key with POST /v1/webhooks/{id}/rotate-key. The caller must be an OWNER or ADMIN. Rotation returns the new secret exactly once, so capture it immediately and store it before you move on.

curl -X POST https://api.example.com/v1/webhooks/{id}/rotate-key \
  -H "Authorization: Bearer atlas_pat_REPLACE_ME"

# Response returns the new secret exactly once:
# { "secret": "atlas_whsec_...", "keyVersion": 2 }

Rotation keeps the previous key valid for 24 hours. During that grace window both the current key and the previous key sign successfully, so you can deploy the new secret to your receiver with zero missed deliveries. Update your receiver to the new secret at your own pace inside the window, then let the old key expire.

While the window is open, verify against your current secret first. If that fails, fall back and verify against the previous secret before rejecting the delivery. The x-atlas-webhook-key-version header tells you which key version signed a given delivery, so you can log the transition and confirm when every delivery has moved to the new key.


Secrets and network

Your signing secret is shown once

The signing secret is returned only at creation and at rotation, and is never retrievable afterward. Atlas stores it encrypted at rest, but you still hold the only usable copy: keep it in a secrets manager, never in source control. If it leaks, rotate immediately.

Endpoints must be public HTTPS

Atlas only delivers to public HTTPS URLs. Requests to localhost, private IP ranges, and other non-public targets are refused to prevent SSRF. To test locally, use a tunneling tool (for example ngrok) that gives you a public HTTPS URL.

On this page

  • Verify every signature
  • Replay protection
  • Key rotation
  • Secrets and network