Verify IllyVoIP webhook signatures in a Node.js receiver

Verify raw-body webhook signatures in Node.js, enforce a timestamp policy and acknowledge events only after durable receipt and duplicate protection.

Verify an IllyVoIP webhook before trusting its event data. The sender signs the timestamp followed by a period and the original JSON bytes using HMAC-SHA256. Parsing and reserializing the JSON before verification can change those bytes and reject a valid request.

This guide is for a backend developer operating an HTTPS receiver. It provides a verification helper and processing sequence, not a complete hosting service. Use a supported Node.js release, secure secret storage and a durable database or queue. The webhook signing secret is separate from your account API key.

Verify an IllyVoIP webhook. Explanatory diagram showing the workflow, not an interface or completed transaction.
Verify an IllyVoIP webhook. Explanatory diagram showing the workflow, not an interface or completed transaction. Open the image for a larger view.

1. Configure the intended endpoint

In Settings → Integrations → Webhooks, select the events the application needs and save its HTTPS URL. Review any SMS sender filters. Keep the endpoint’s signing secret in the backend’s secret store; do not put it in browser JavaScript, a public URL or a sample request shared with others.

Confirm which endpoint and environment a test belongs to. Several endpoints can receive the same business event with different signing secrets. The delivery log helps correlate an attempt, but a successful HTTP acknowledgement does not prove your application finished processing it.

2. Preserve the request bytes

Read the Illyvoip-Signature header and raw body before ordinary JSON parsing. In an Express application, register the webhook route with an appropriate raw-body parser before a global JSON parser can consume it. Apply a deliberate request-size limit and reject unsupported content types.

Do not calculate a signature over JSON.stringify(req.body). Whitespace, escaping and property order can differ even when the parsed object looks equivalent. Reverse proxies and middleware must not transform the signed body unnoticed.

3. Verify the signature and time policy

The current header has the form t=TIMESTAMP,v1=HEX_DIGEST. This helper accepts one timestamp and one current-version digest, rejects malformed input and compares equal-length binary digests:

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

export function verifyIllyvoip(rawBody, signature, secret, nowSeconds,
                              toleranceSeconds = 300) {
  if (!Buffer.isBuffer(rawBody) || typeof signature !== 'string' ||
      typeof secret !== 'string' || secret.length === 0 ||
      !Number.isSafeInteger(nowSeconds) ||
      !Number.isSafeInteger(toleranceSeconds) || toleranceSeconds < 0) return false;
  const fields = signature.split(',').map(part => part.trim());
  const timestamps = fields.filter(part => part.startsWith('t='));
  const digests = fields.filter(part => part.startsWith('v1='));
  if (timestamps.length !== 1 || digests.length !== 1) return false;
  const stamp = timestamps[0].slice(2);
  const hex = digests[0].slice(3);
  if (!/^[0-9]+$/.test(stamp) || !/^[a-fA-F0-9]{64}$/.test(hex)) return false;
  const timestamp = Number(stamp);
  if (!Number.isSafeInteger(timestamp) ||
      Math.abs(nowSeconds - timestamp) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret)
    .update(stamp + '.', 'utf8').update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(hex, 'hex'));
}

The five-minute tolerance is an example receiver policy, not an IllyVoIP delivery deadline. Choose it deliberately and keep the server clock accurate. A timestamp too far in the future also fails the example policy. The current sender signs each delivery attempt using that attempt’s timestamp, so event creation time and signature time are different concepts.

Call the helper with the original Buffer, header, correct endpoint secret and Math.floor(Date.now() / 1000). A false result should stop event processing. Record a sanitized rejection reason internally without logging the secret, full signature or sensitive body.

4. Parse and persist the authenticated event

  1. After verification succeeds, parse the same raw bytes as JSON.
  2. Validate the expected event structure and supported event type.
  3. Use the body’s id as the event deduplication key, scoped to your intended account/application.
  4. Atomically store the event and its processing job, or use a transactional outbox.
  5. Return a prompt 2xx acknowledgement only after durable acceptance succeeds.

A database unique constraint should resolve concurrent duplicate deliveries. If a verified event already exists durably, acknowledge it without creating another business action. If the database or queue is unavailable, do not return success and silently discard the event.

Illyvoip-Delivery identifies a delivery job; it does not replace the body event ID. Do not treat a delivery ID as an SMS message ID either. Follow the documented message fields when correlating a notification with an accepted send.

5. Process once at the business layer

Let a worker claim the stored job. Make the resulting business action idempotent as well: recording one event twice must not create two reminders, two orders or two follow-up calls. A unique inbox row alone does not protect an external side effect if the worker crashes after performing it but before marking completion.

Retain an explicit processing state and the downstream operation identity. Separate “received”, “processing”, “completed” and “needs review”. Keep personal event data only for the period needed by your application.

6. Test failures before enabling real traffic

Generate local signed fixtures with a test secret. Check valid input, one changed byte, changed JSON spacing, the wrong secret, malformed hex, duplicate timestamp/digest fields, stale and future timestamps, and a non-Buffer body. Then test two concurrent copies of the same event and a storage outage.

For an authorized live webhook, compare the Portal delivery log with your receiver’s durable event record. A 2xx response confirms acceptance by the receiver; check the worker outcome separately. Do not intentionally trigger paid messages solely to test parsing when a local fixture answers that question.

Frequently asked questions

Is an IP allowlist enough?

No. Use the current delivery-IP information where appropriate and still verify signatures.

Does rotating the API key rotate this secret?

Do not assume so. Manage the webhook endpoint’s signing secret through its own workflow and coordinate the receiver update.

Related guides and references

Reviewed 8 October 2026 using current IllyVoIP implementation and official references. Check your installed version and account options before applying settings. Examples do not certify a physical device or completed live integration.