Interop recipes
These recipes assume you have already read Webhooks: the
verification handshake, the barakah.http-message-signatures.v2 profile, the
legacy Standard Webhooks profile, and identity/retry/replay semantics. They
are vendor-neutral by design: no Barakah connector or SDK exists for any of
these platforms, and none is coming. Everything below is a plain HTTP
receiver plus the same signature verification you would write for any other
webhook.
n8n: raw-body verification
Section titled “n8n: raw-body verification”The Webhook node parses JSON by default, which discards the exact bytes the
signature was computed over. Binary mode is required for verification; use
structured mode (application/cloudevents+json) only until the raw-body path
is confirmed working, per Webhooks.
-
Webhook node - set Response Mode to “Using Respond to Webhook Node” (so verification confirmation can be synchronous) and, critically, enable Raw Body (or Binary Data, depending on your n8n version) so
$binary.dataor the raw body property holds the untouched bytes. Do not let n8n auto-parse the body as JSON before your Code node runs. -
Code node - import the same verifier the portal publishes (verify-webhook-v2.mjs) and call it with the raw bytes, not
$json:import { verifyBarakahWebhook } from './verify-webhook-v2.mjs';const rawBody = Buffer.from($binary.data.data, 'base64'); // exact bytes n8n receivedconst headers = $request.headers; // n8n lower-cases header names alreadyconst verified = verifyBarakahWebhook({rawBody,requestHeaders: headers,expectedSubscriptionId: 'YOUR_SUBSCRIPTION_ID',pinnedPublicKey: 'whpk_...', // from get_webhook_subscription, never from the callback});return [{ json: verified.data }];n8n’s Code node environment does not always support ES module
import- if yours does not, inline the verifier’s logic or load it withrequire()after converting it to CommonJS; do not skip verification. -
Only after
verifyBarakahWebhookreturns without throwing should the workflow act onverified.data. A thrown error means reject the request; do not swallow it and continue.
A 2xx response from the Webhook node only proves the HTTP request was received. It does not prove that a downstream node (Gmail, Sheets, a database write) actually completed - see the receiver checklist in Webhooks. Make the workflow durably persist or complete its own side effect before you consider the delivery handled.
Generic Ed25519 verification (Node.js and Python)
Section titled “Generic Ed25519 verification (Node.js and Python)”Use these when the platform is not n8n/Activepieces/Make/Zapier - any
framework that hands you a raw request body and headers. The header names
below are the exact ones the platform sends and the ones
verify-webhook-v2.mjs checks: signature-input, signature,
content-digest, webhook-id, webhook-timestamp, barakah-subscription-id,
barakah-key-id, plus the message-kind-specific identity headers
(barakah-event-id/barakah-delivery-id/barakah-attempt-id/
barakah-replay-generation for events, barakah-probe-id for
verification callbacks). Do not invent alternate names - see
Covered components for the full ordered
list per wire shape.
Node.js
Section titled “Node.js”Prefer the published verifier directly:
import { verifyBarakahWebhook } from './verify-webhook-v2.mjs';
const verified = verifyBarakahWebhook({ rawBody, // Buffer/Uint8Array of the untouched request body requestHeaders: req.headers, // lower-cased header map expectedSubscriptionId: subscriptionId, pinnedPublicKey: currentPublicKey, // whpk_... from get_webhook_subscription});If you need to verify the raw Ed25519 signature yourself (for example inside
a platform that cannot load a local module), the primitive is
crypto.verify(null, message, publicKey, signature) (Node’s built-in
node:crypto, Ed25519 needs no digest algorithm argument) against the
@signature-params-terminated message described in
HTTP Message Signatures v2. Do not
reimplement the covered-component ordering or clock-window checks from
scratch - copy them from the published verifier; they are the part most
likely to be gotten subtly wrong.
Python
Section titled “Python”import base64import hashlibimport hmacfrom cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def verify_signature(raw_body: bytes, signature_base: str, signature_b64: str, public_key_whpk: str) -> bool: # public_key_whpk is "whpk_" + standard base64 of the raw 32-byte key raw_key = base64.b64decode(public_key_whpk.removeprefix("whpk_")) public_key = Ed25519PublicKey.from_public_bytes(raw_key) signature = base64.b64decode(signature_b64) try: public_key.verify(signature, signature_base.encode("utf-8")) return True except Exception: return False
def content_digest(raw_body: bytes) -> str: return "sha-256=:" + base64.b64encode(hashlib.sha256(raw_body).digest()).decode() + ":"Build signature_base from the exact ordered covered components for the
message’s content type and kind (see
Covered components), each line formatted as
"<component>": <value> (lower-case component name, @method value is the
literal string POST), terminated by a "@signature-params": ... line
copied verbatim from the parsed Signature-Input value for the label you are
checking. Verify content-digest against content_digest(raw_body) before
trusting anything else. Reject on any mismatch - do not fall back to a
partial or best-effort check.
Key rotation
Section titled “Key rotation”get_webhook_subscription returns the current whpk_ public key and, during
an active rotation window, the previous one. Pin both when present, and
accept whichever member (barakah-current or barakah-previous) of the
Signature-Input/Signature dictionaries matches. Never trust a key that
only appears in the callback itself - see
Legacy Standard Webhooks signature
and HTTP Message Signatures v2 for
the full rotation contract.
Make / Zapier-class code steps
Section titled “Make / Zapier-class code steps”Platforms in this class expose one generic “run JavaScript” step against the raw webhook payload. The template below is a single function you paste in and adapt to the platform’s exact input/output binding:
function verifyAndUnwrap(input) { const { verifyBarakahWebhook } = require('./verify-webhook-v2.mjs'); // or inline the logic
const rawBody = Buffer.from(input.rawBody, 'base64'); // however the platform hands you exact bytes const headers = input.headers; // must be the exact, lower-cased header set the platform received
const verified = verifyBarakahWebhook({ rawBody, requestHeaders: headers, expectedSubscriptionId: input.subscriptionId, pinnedPublicKey: input.pinnedPublicKey, });
return { data: verified.data, eventId: verified.eventId, messageKind: verified.messageKind };}The reminder that matters here: many of these platforms re-serialize the incoming JSON body before your code step ever sees it (their trigger normalizes the payload for the visual editor). A re-serialized body is not the signed body - key order, whitespace, and numeric formatting differences all break the Ed25519 check even though the content looks identical. Before relying on one of these platforms in production, confirm its trigger step exposes the untouched raw bytes (often a “raw body” or “advanced” trigger option, sometimes only available on a paid tier); if it does not, use structured mode and still verify against whatever raw bytes the platform does expose, and treat any platform that offers neither as unable to verify signatures at all - do not silently skip verification to keep the workflow running.
Activepieces: same mechanism, raw body already exposed
Section titled “Activepieces: same mechanism, raw body already exposed”Activepieces is the one platform in this class whose trigger hands you the
exact bytes without any extra option. Its Catch Webhook trigger emits the
whole request it received as the trigger output: method, headers (all
lower-cased), body (parsed JSON), queryParams, and rawBody, the
untouched request body as a string. rawBody is the same field the trigger’s
own HMAC option hashes, so it is the signed bytes, not a re-serialization.
Two consequences:
- Leave the trigger’s Authentication on “None”. The built-in options (Basic, Header, HMAC) cannot check an Ed25519 signature, and a mismatched setting silently drops every request without starting a run - you would see nothing, not an error.
- Verify in a Code step using the template above. Map the step inputs to
{{trigger['output'].rawBody}}and{{trigger['output'].headers}}, and feed the signature check the raw string as UTF-8 bytes:
export const code = async (inputs) => { const rawBody = Buffer.from(inputs.rawBody, 'utf8'); // never JSON.stringify(inputs.body) const headers = inputs.headers; // already lower-cased by Activepieces const verified = verifyBarakahWebhook({ rawBody, requestHeaders: headers, expectedSubscriptionId: inputs.subscriptionId, pinnedPublicKey: inputs.pinnedPublicKey, }); return { data: verified.data, eventId: verified.eventId, messageKind: verified.messageKind };};Verification handshake. Barakah’s verification message arrives at the
same URL as events. Give the subscription the trigger’s /sync URL variant,
so the flow’s response reaches Barakah, and answer the verification message
with a Return Response step (JSON, status 200) whose body is the
trigger’s body - {{trigger['output'].body}} is the closed verification
object Barakah expects back. Branch on messageKind from the Code step (or on
the presence of challenge in the body) so event deliveries do not get the
same echo. The /test URL variant only generates sample data and never runs
the published flow, which makes it a safe way to inspect the raw payload
shape before wiring the check.
Loop safety is unchanged: a Code step that writes back through MCP should
send a correlation_id and drop events whose barakahoriginconnection is its
own connection (see below).
Replay as a test fire
Section titled “Replay as a test fire”replay_webhook_events (MCP, webhooks:write plus events:read) is the
supported way to exercise a receiver end to end without waiting for a real
event to occur naturally: point it at exact event IDs (from
list_integration_events, get_integration_event, or
list_webhook_deliveries) or an open gap ID (from list_webhook_gaps), and
it queues a fresh delivery attempt against your current endpoint.
The repair-then-replay order matters. If the receiver was ever unreachable,
misconfigured, or paused, fix the endpoint (rotate keys, redeploy, resolve
receiver_gone/verification failures) and confirm the subscription is
active again before replaying - automatic retries would have already
exhausted the 72-hour window for anything not covered by an open gap, so a
stale endpoint will just fail the replay the same way it failed the
original delivery.
Idempotency: a replay is a genuinely new delivery, even when it carries
the same CloudEvent id/webhook-id as the original. It gets a new
barakah-delivery-id and a new barakah-replay-generation; only the
underlying event identity and, for a subscription-scoped read, the
barakahsubscriptionsequence stay the same. Practically:
- Deduplicate automatic HTTP retries by the stable
barakah-delivery-id(retries of the same delivery keep that ID; onlybarakah-attempt-id,webhook-timestamp, and the signature change). - Never deduplicate an explicit replay by CloudEvent ID alone - a
receiver that does will silently ignore a replay you triggered on purpose.
If your receiver needs replay to be a genuine no-op re-run (rather than
executing side effects twice), key its own idempotency ledger off the new
barakah-delivery-id/barakah-replay-generation, not the event ID. - A receiver that intentionally wants to re-execute on replay (the common case when replay is being used as a deliberate test fire, or to recover a gap by re-running the original side effect) needs no special handling beyond the normal signature/identity checks - that is the default behavior of “a replay is a new delivery.”
See Identity, retries, and replay for the full identity model this relies on.
Loop safety (D7 origin attribution)
Section titled “Loop safety (D7 origin attribution)”Every published CloudEvent carries three origin extension attributes, named
exactly as shown (structured representation; the equivalent ce- prefixed
headers exist in binary mode):
barakahoriginkind- one ofuser,mcp,system, ordevice.barakahoriginconnection- an opaque string identifying the writing connection. It is stable for events written by the same connection and is not a human-readable identifier.barakahcorrelationid- optional. Present when the writer (or the MCP tool call that triggered a server-side write) supplied a callercorrelation_id.
A consumer that reacts to a webhook by writing back into Barakah through MCP is the classic loop hazard: it receives an event, calls an MCP write tool, which publishes a new event, which the same subscription may deliver right back to it. The fix has two parts:
- Send
correlation_idon every MCP write your automation makes in response to an event, and remember the IDs you minted. - Drop any incoming event whose
barakahoriginconnectionequals your own connection ref, or whosebarakahcorrelationidis one you minted yourself. Everything else should be processed normally.
A minimal, dependency-free implementation of that rule, shouldProcess, is
published as loop-safety.mjs - import it the
same way as the verifier:
import { shouldProcess } from './loop-safety.mjs';
if (!shouldProcess(event, { ownConnectionRef, mintedCorrelationIds })) { return; // this is an echo of our own write; ignore it}device-origin events are not server-verified presence. Events like
notification.action_completed and proximity.arrived/proximity.left
carry barakahoriginkind: "device" because they originate from a
device-reported observation (a notification action, a geofence trigger),
never from a server-side confirmation of where a device physically is.
Treat them as what a device claimed happened, not as ground truth, and apply
the same loop-safety rule to them as to any other origin kind - device is
orthogonal to whether an event is a self-authored echo.