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:
- Cloud SMS webhooks — see Message Webhooks
- Cloud Voice webhooks — see Voice Webhooks
- VoiceML requests — see VoiceML requests
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:
- Read
X-Enfonica-Signaturefrom the request headers. - Recompute the signature from the URL, the
X-Enfonica-Eventheader, and the raw body using the formula above. - 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);
}