Notifly

HTTP request step signing

Every call from an HTTP Request step carries a signed header. See its format, what is signed and how to verify it on your server with a short Node example.

The HTTP Request step calls an endpoint you own from inside a workflow. Because anyone could send a request to that URL, every request from the step carries a signature that proves it came from your Notifly environment and was not altered. Your endpoint should verify it before acting on the request.

You add the step from the Actions group of the workflow editor's add-step menu. In its Headers list the dashboard shows a read-only novu-signature row with the value <calculated when request is sent>: the header is added automatically at send time and you do not configure it. The header keeps the novu- prefix because Notifly's workflow engine descends from Novu; the name is what your code reads.

The header

novu-signature: t=1760000000000,v1=3b1f...e9
  • t is the time the request was signed, in milliseconds since the Unix epoch.
  • v1 is a lowercase hex HMAC-SHA256.

The HMAC is computed over the string <t>.<body> using the secret key of the environment the workflow runs in (the key shown under API Keys) as the HMAC key. Development and Production sign with different keys.

<body> is the JSON text of the request body, exactly as sent. A request that sends no body, which is every GET, DELETE, HEAD and OPTIONS request and any request whose body is empty, is signed with the body {}, so verify those against the string {}.

Verify a request

Verify against the raw bytes you received, before any JSON parsing and re-serializing, because the signature covers the exact text. Reject requests whose timestamp is far from your clock to limit replays, and compare in constant time.

import { createHmac, timingSafeEqual } from 'node:crypto';

const MAX_AGE_MS = 5 * 60 * 1000;

export function verifyNotiflySignature(rawBody: string, header: string, secretKey: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
  const timestamp = parts.t;
  const received = parts.v1;

  if (!timestamp || !received) return false;
  if (Math.abs(Date.now() - Number(timestamp)) > MAX_AGE_MS) return false;

  const expected = createHmac('sha256', secretKey).update(`${timestamp}.${rawBody}`).digest('hex');
  const a = Buffer.from(received, 'hex');
  const b = Buffer.from(expected, 'hex');

  return a.length === b.length && timingSafeEqual(a, b);
}

In an Express handler, capture the raw body (for example with express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8'); } })), call verifyNotiflySignature(req.rawBody ?? '{}', req.get('novu-signature') ?? '', process.env.NOTIFLY_SECRET_KEY!), and answer 401 when it returns false. For a request that has no body, pass '{}'.

Keep the secret key on your server. It is the same value you use as the ApiKey for the REST API, so never put it in browser or mobile code.

Good to know

  • The URL must use http or https and carry no embedded credentials, and it must point at a public endpoint. Notifly rejects localhost, metadata.google.internal, single-label host names such as api, host names ending in .internal, .local or .lan, and private or link-local IP addresses. It also resolves the host name before connecting and rejects it if any address is private or link-local. A rejected step fails with an error in the activity feed and nothing is sent.
  • Redirects are followed up to three times, and every target is checked the same way. When a redirect moves to a different host, the novu-signature and Authorization headers, cookies and the request body are not forwarded, and 307/308 redirects to a different host are refused. Respond directly from the endpoint and do not rely on redirects as a trust boundary.
  • The step's method defaults to POST and its timeout to 5000 ms.
  • Your endpoint should answer with a JSON object so later steps can use its response.
  • The step editor's test request is a real request signed the same way, with the secret key of the environment you are editing in, so you can check your verification before you publish.

On this page