Generic webhook
Fromenance is a communication provenance platform, and the generic adapter covers any system that can make an HTTP call after a send: a core banking platform, a print and mail vendor’s notification feed, an internal queue consumer. You post one JSON event per send, signed with the source secret, and Fromenance registers it.
By the end you will have a generic source, a signed POST in your send path, and events showing on the source’s health panel.
1. Create the source
Section titled “1. Create the source”curl -X POST https://api.fromenance.com/v1/sending-sources \ -H "Authorization: Bearer fr_live_..." \ -H "Content-Type: application/json" \ -d '{"name":"Core banking notices","kind":"generic","expected_daily_volume":5000,"silence_alert_minutes":720}'The response includes webhook_url (https://api.fromenance.com/v1/hooks/esp/src_...) and secret (whsec_...), shown once.
2. Post send events
Section titled “2. Post send events”Body: one event, or { "events": [ ... ] }.
| Field | Required | Notes |
|---|---|---|
message_id |
yes | Your message id or the RFC 5322 Message-ID |
recipient |
yes | The recipient address. Hashed with your tenant secret on arrival and never stored |
sent_at |
no | ISO 8601; defaults to now |
from |
no | The visible From address |
code |
no | A code you reserved and placed in the footer. Completes the reservation |
template_id, campaign_id |
no | Your identifiers |
html, text, subject |
no | The rendered message. Fingerprinted on arrival and not stored. Include it when you can; it is what lets a forward without a readable code still match |
Header: X-Fromenance-Signature: sha256=<hex HMAC-SHA256 of the raw request body keyed with the source secret>. Sign the exact bytes you send; re-serializing JSON on either side changes the signature.
Response: { "ok": true, "registered": n, "completed": n, "ignored": n }. ignored counts events for a message and recipient already registered, so redelivery is harmless.
/** * Generic webhook adapter: post a send event to Fromenance from any system that can make an HTTP call. * The raw body is signed with HMAC-SHA256 using the source secret and sent as * X-Fromenance-Signature: sha256=<hex> * * Environment: * FROMENANCE_SOURCE_WEBHOOK_URL https://api.fromenance.com/v1/hooks/esp/src_... (shown when the source is created) * FROMENANCE_SOURCE_SECRET whsec_... (shown once when the source is created) */import { createHmac } from "node:crypto";
export interface GenericSendEvent { message_id: string; recipient: string; sent_at?: string; from?: string; /** A reserved verify code you placed in the footer. Completes the reservation. */ code?: string; template_id?: string; campaign_id?: string; /** Optional rendered body so Fromenance can fingerprint it. The body is fingerprinted and not stored. */ html?: string; text?: string; subject?: string;}
export function signGenericEvent(secret: string, rawBody: string): string { return `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;}
export async function postSendEvents( webhookUrl: string, secret: string, events: GenericSendEvent[], f: typeof fetch = fetch,): Promise<{ registered: number; completed: number; ignored: number }> { const rawBody = JSON.stringify(events.length === 1 ? events[0] : { events }); const res = await f(webhookUrl, { method: "POST", headers: { "content-type": "application/json", "x-fromenance-signature": signGenericEvent(secret, rawBody), }, body: rawBody, }); if (!res.ok) throw new Error(`hook rejected: ${res.status} ${await res.text()}`); return (await res.json()) as { registered: number; completed: number; ignored: number };}
export async function main(): Promise<void> { const url = process.env.FROMENANCE_SOURCE_WEBHOOK_URL; const secret = process.env.FROMENANCE_SOURCE_SECRET; if (!url || !secret) throw new Error("Set FROMENANCE_SOURCE_WEBHOOK_URL and FROMENANCE_SOURCE_SECRET"); const result = await postSendEvents(url, secret, [ { message_id: `<${crypto.randomUUID()}@northfieldbank.example>`, recipient: "jane.doe@example.com", sent_at: new Date().toISOString(), from: "alerts@northfieldbank.example", template_id: "fraud-alert-v3", html: "<p>We noticed a sign in from a new device.</p>", }, ]); console.log(result);}Equivalent with curl:
BODY='{"message_id":"<n-1042@northfieldbank.example>","recipient":"jane.doe@example.com","sent_at":"2026-09-24T14:42:10Z","from":"notices@northfieldbank.example","template_id":"rate-change-v1"}'SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$FROMENANCE_SOURCE_SECRET" | awk '{print $NF}')curl -X POST "$FROMENANCE_SOURCE_WEBHOOK_URL" \ -H "Content-Type: application/json" \ -H "X-Fromenance-Signature: sha256=$SIG" \ --data "$BODY"Verification
Section titled “Verification”Fromenance recomputes the HMAC over the raw body with the source secret and compares in constant time. A missing or wrong signature returns 401 and increments signature_failures on the source. There is no timestamp in this scheme; if replay of a captured event matters in your environment, include sent_at and treat the ignored count as the dedupe signal, or use the SDK path instead, which authenticates with your API key.
Reserve flow
Section titled “Reserve flow”When the footer must carry the code before the send exists, reserve first and pass the code in the event:
POST /v1/communications/reservewithrecipient_hashandtemplate_id, getverify_code.- Render the footer with the code and send.
- Post the event with
codeset. The reservation is completed with the message id and time; if you includehtmlortext, the fingerprint is added too.
If you render the message in code you control, @fromenance/sdk register() does all of this in one call without a source. The generic adapter is for the systems where that is not possible.
Source health
Section titled “Source health”silent is raised when no event arrives within silence_alert_minutes (default 1440); set it to a value longer than your quietest sending window. coverage_gaps counts reservations that expired after 24 hours without an event. Rotate the secret with POST /v1/sending-sources/{id}/rotate-secret; the old secret stops working immediately.