Skip to content

Request signing

Enfonica signs every outbound webhook and VoiceML request with an HMAC-SHA256 signature so your application can verify the request came from Enfonica and was not modified in transit. The signature is delivered in the X-Enfonica-Signature header. This page is the canonical reference for the signing algorithm and includes copy/paste reference implementations.

The same signing scheme is used for:

The signing algorithm

The signature is computed as:

signature = Base64( HMAC-SHA256( signing_key_bytes, UTF8( originalUrl + eventHeader + payload ) ) )

Where:

Input Definition
signing_key_bytes Your project's signing key as 64 raw bytes — see The signing key below.
originalUrl The full URL of the request, including scheme, host, path, and any query string.
eventHeader The exact value of the X-Enfonica-Event header on the request (for example, INCOMING_MESSAGE or CALL_STATE_UPDATE). For VoiceML requests this value is CALL.
payload The raw request body, byte-for-byte. Do not re-serialize the JSON before hashing — a single whitespace difference will change the signature.

To verify a received request:

  1. Read X-Enfonica-Signature from the request headers.
  2. Recompute the signature from the URL, the X-Enfonica-Event header, and the raw body using the formula above.
  3. Compare the two values using a constant-time comparison to avoid leaking information through timing. The reference implementations below do this for you.

The signing key

Each Enfonica project has a single signing key. You can find it in the Enfonica Console under Project → Settings.

The signing key is 64 raw bytes. The Enfonica Console displays it as the base64 encoding of those 64 bytes — an 88-character string ending in ==.

Common pitfall: base64-decode the Console value first

The string you copy from the Enfonica Console is not the signing key — it is the base64 encoding of the signing key. If you pass the 88-character string straight into HMAC-SHA256, every signature you compute will be wrong.

Always base64-decode the Console value to get the 64-byte key before passing it to HMAC-SHA256:

byte[] signingKey = Convert.FromBase64String(consoleValue);
const signingKey = Buffer.from(consoleValue, 'base64');

Test vector

Use the following reference values to verify your implementation against the algorithm above. Applying the algorithm to the given inputs produces the expected signature.

Field Value
Signing key (as displayed in the Console) AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==
Signing key (decoded) The 64 bytes 0x00, 0x01, 0x02, ..., 0x3F
Original URL https://example.com/webhook?token=abc123
X-Enfonica-Event INCOMING_MESSAGE
Payload (truncated dummy body) {"name":"projects/example/messages/abc","body":"Hi"}
Expected X-Enfonica-Signature cmsZUX+1UxBNoOaOmhzwGWX9bw/bkBKN3GQxfGx4ra8=

Reference implementations

The implementations below take the signing key as a raw byte array. Each one exposes a Calculate function (used when sending a signed request) and a Verify function (used when receiving one). Verify uses a constant-time comparison so an attacker cannot learn anything about the correct signature by measuring how long verification takes.

using System;
using System.Security.Cryptography;
using System.Text;

namespace Enfonica.Signing.Examples;

public static class SignatureUtility
{
    public static string CalculateSignature(byte[] signingKey, string originalUrl, string eventHeader, string payload)
    {
        byte[] message = Encoding.UTF8.GetBytes(originalUrl + eventHeader + payload);
        using var hmac = new HMACSHA256(signingKey);
        byte[] hash = hmac.ComputeHash(message);
        return Convert.ToBase64String(hash);
    }

    public static bool VerifySignature(byte[] signingKey, string originalUrl, string eventHeader, string payload, string providedSignature)
    {
        if (string.IsNullOrEmpty(providedSignature))
        {
            return false;
        }

        byte[] providedBytes;
        try
        {
            providedBytes = Convert.FromBase64String(providedSignature);
        }
        catch (FormatException)
        {
            return false;
        }

        byte[] message = Encoding.UTF8.GetBytes(originalUrl + eventHeader + payload);
        using var hmac = new HMACSHA256(signingKey);
        byte[] expected = hmac.ComputeHash(message);

        return CryptographicOperations.FixedTimeEquals(expected, providedBytes);
    }
}
import { createHmac, timingSafeEqual } from 'node:crypto';

export function calculateSignature(
  signingKey: Buffer,
  originalUrl: string,
  eventHeader: string,
  payload: string,
): string {
  const hmac = createHmac('sha256', signingKey);
  hmac.update(originalUrl + eventHeader + payload, 'utf8');
  return hmac.digest('base64');
}

export function verifySignature(
  signingKey: Buffer,
  originalUrl: string,
  eventHeader: string,
  payload: string,
  providedSignature: string,
): boolean {
  if (!providedSignature) {
    return false;
  }

  let providedBytes: Buffer;
  try {
    providedBytes = Buffer.from(providedSignature, 'base64');
  } catch {
    return false;
  }

  const hmac = createHmac('sha256', signingKey);
  hmac.update(originalUrl + eventHeader + payload, 'utf8');
  const expected = hmac.digest();

  if (expected.length !== providedBytes.length) {
    return false;
  }
  return timingSafeEqual(expected, providedBytes);
}