Quickstart
Fromenance is a communication provenance platform: you register each outbound message at send time and customers can ask whether a message is real. This quickstart takes an empty tenant to a Verified reply in about 15 minutes.
There is nothing to install. Fromenance is a REST API: one HTTPS call at send time, one TXT record, one redirect rule.
At the end you will have an API key, a verified domain, a working redirect rule, one registered communication with its footer, and a reply in your inbox that says the message matches a communication you registered. For the complete, agent executable version of these steps with every request and response field, see the Integration guide.
You need: an owner or admin login to app.fromenance.com, DNS access for your domain, admin access to your mail platform, and either curl, Node 18+, Bun, or Python 3.9+. Replace jtmerlin.example with your domain and jtmerlin with your tenant slug throughout.
-
Create an API key.
In the admin app open Settings, API keys, and mint a key named
quickstartwith modetestand scoperegister. The secret is shown once. Test keys (fr_test_) write to a sandbox partition and never mail a real customer: sandbox replies go to the fraud contact address set in Settings, so set that to your own address now. Copy the recipient hashing secret from Settings as well; it is separate from the API key.You can also mint the key from the API with an existing admin key:
Terminal window curl -X POST https://api.fromenance.com/v1/api-keys \-H "Authorization: Bearer fr_live_..." \-H "Content-Type: application/json" \-d '{"name":"quickstart","mode":"test","scopes":["register"]}'Export the values the samples read:
Terminal window export FROMENANCE_API_KEY=fr_test_...export FROMENANCE_TENANT_SECRET=... # the recipient hashing secret from Settingsexport FROMENANCE_API_ORIGIN=https://api.fromenance.com -
Add your domain and publish the TXT record.
Terminal window curl -X POST https://api.fromenance.com/v1/domains \-H "Authorization: Bearer $FROMENANCE_API_KEY" \-H "Content-Type: application/json" \-d '{"domain":"jtmerlin.example"}'The response includes
txt_record. Publish it at your DNS provider:_fromenance.jtmerlin.example. TXT "v=fromenance1; t=jtmerlin; k=<32 hex characters>"Fromenance polls DNS every hour for 24 hours. Force a check and confirm
statusisverified:Terminal window curl -X POST https://api.fromenance.com/v1/domains/dom_.../check \-H "Authorization: Bearer $FROMENANCE_API_KEY"The domain gets the roles
trustandsenderby default.trustlets forwards that authenticate for it through;senderlets it appear as a registered From address. -
Create the redirect rule.
Create the mailbox or group
verify@jtmerlin.exampleand add a rule that redirects (not forwards) everything it receives to your tenant inbox,jtmerlin@verify.fromenance.com. The address is shown asverify_addressonGET /v1/tenant.Redirect keeps the customer as the sender, so Fromenance can reply to them, and preserves the DKIM signature your platform adds. A forward rewrites the sender to
verify@and the reply would come back to you.Click by click guides: Google Workspace, Microsoft 365, Proofpoint, Mimecast, Cisco Secure Email.
-
Send the forwarding test.
Terminal window curl -X POST https://api.fromenance.com/v1/domains/dom_.../forwarding-test \-H "Authorization: Bearer $FROMENANCE_API_KEY"Fromenance emails
verify@jtmerlin.examplewith a token. When the redirect delivers it to your tenant inbox with a DKIM or ARC pass for your domain, the rule is marked Working. Poll untilstatusisworking:Terminal window curl https://api.fromenance.com/v1/domains/dom_.../forwarding-test \-H "Authorization: Bearer $FROMENANCE_API_KEY"A
failedresult names the rejection reason. The common one isauth_failed, which means the rule forwarded instead of redirecting. -
Register the first communication.
One
POST /v1/communicationsper recipient at send time. You send two hashes, never the address or the body:recipient_hash(HMAC-SHA256 of the lowercased, trimmed address keyed with your tenant secret) and, optionally,content_fingerprint(a SimHash and SHA-256 of the normalized body). The response carries the verify code for the footer.Plain
fetch, no dependencies.fingerprint.tsandrecipient-hash.tsnext to it are the two helpers it imports; both are on the Integration guide and in the docs repository.samples/quickstart.ts /*** Quickstart: register one rendered email with plain fetch and render the footer. Nothing to install.* Runs on Node 18+ (`npx tsx samples/quickstart.ts`) or Bun (`bun run samples/quickstart.ts`).** Environment:* FROMENANCE_API_KEY fr_test_... from Settings, API keys (scope: register)* FROMENANCE_TENANT_SECRET the recipient hashing secret from Settings* FROMENANCE_API_ORIGIN optional, defaults to https://api.fromenance.com** The recipient address and the body are hashed in this process. Only the HMAC and the fingerprint are sent.*/import { fingerprintContent, fingerprintSubject } from "./fingerprint.ts";import { recipientHash } from "./recipient-hash.ts";export interface RegistrationResponse {id: string;verify_code: string;expires_at: string;sandbox: boolean;}export interface Problem {type: string;title: string;status: number;detail?: string;instance?: string;errors?: { path: string; message: string }[];request_id?: string;}const API = (process.env.FROMENANCE_API_ORIGIN ?? "https://api.fromenance.com").replace(/\/$/, "");/** One call at send time. Throws with the RFC 9457 problem document when the API rejects the request. */export async function register(apiKey: string,body: Record<string, unknown>,idempotencyKey: string,): Promise<RegistrationResponse> {const res = await fetch(`${API}/v1/communications`, {method: "POST",headers: {authorization: `Bearer ${apiKey}`,"content-type": "application/json","idempotency-key": idempotencyKey,},body: JSON.stringify(body),});if (!res.ok) {const problem = (await res.json()) as Problem;throw new Error(`${problem.status} ${problem.title}: ${problem.detail ?? ""} (request ${problem.request_id})`,);}return (await res.json()) as RegistrationResponse;}/** The footer text. The code appears twice on purpose so one copy survives rewrapping and screenshot OCR. */export function footer(code: string,institution: string,verifyAddress: string,verifyPageUrl: string,): string {const page = verifyPageUrl.replace(/^https?:\/\//, "");return [`Not sure this email is from ${institution}? Forward it to ${verifyAddress}`,`or enter code ${code} at ${page}. Reference: ${code}`,].join("\n");}export async function main(): Promise<{ id: string; verifyCode: string; footer: string }> {const apiKey = process.env.FROMENANCE_API_KEY;const tenantSecret = process.env.FROMENANCE_TENANT_SECRET;if (!apiKey || !tenantSecret) throw new Error("Set FROMENANCE_API_KEY and FROMENANCE_TENANT_SECRET");// The email exactly as your sending system renders it, before the footer is added.const messageId = `<${crypto.randomUUID()}@jtmerlin.example>`;const to = "jane.doe@example.com";const subject = "A new device signed in to your account";const html = `<p>Hi Jane,</p><p>A new device signed in to your JT Merlin Bank account on September 24 at 10:42 AM.</p><p>If this was you, no action is needed. If not, open the app and lock your card.</p>`;// Hash and fingerprint locally. Neither the address nor the body is sent.const fp = await fingerprintContent({ html });const body = {channel: "email",recipient_hash: await recipientHash(to, tenantSecret),message_id: messageId,from_address: "alerts@jtmerlin.example",sent_at: new Date().toISOString(),subject_fingerprint: await fingerprintSubject(subject),content_fingerprint: fp.tokens > 0 ? { simhash: fp.simhash, sha256: fp.sha256 } : undefined,link_domains: ["jtmerlin.example"],template_id: "new-device-alert-v2",provider: "api",};// The message id is a good Idempotency-Key: a retried send registers once.const registration = await register(apiKey, body, messageId);// Paste this footer into the message before it goes out.const text = footer(registration.verify_code,"JT Merlin Bank","verify@jtmerlin.example","https://jtmerlin.example/verify",);console.log(`registered ${registration.id} with code ${registration.verify_code}`);console.log(text);return { id: registration.id, verifyCode: registration.verify_code, footer: text };}if (process.argv[1]?.endsWith("quickstart.ts")) {main().catch((err) => {console.error(err);process.exit(1);});}Terminal window bun run samples/quickstart.ts# or: npx tsx samples/quickstart.tsrecipient_hashishmac-sha256:plus the lowercase hex HMAC-SHA256 of the lowercased, trimmed address keyed with your tenant secret. The content fingerprint is optional; without it, matching relies on the code plus the recipient.Terminal window RECIPIENT_HASH="hmac-sha256:$(printf 'jane.doe@example.com' \| openssl dgst -sha256 -hmac "$FROMENANCE_TENANT_SECRET" | awk '{print $NF}')"curl -X POST https://api.fromenance.com/v1/communications \-H "Authorization: Bearer $FROMENANCE_API_KEY" \-H "Content-Type: application/json" \-H "Idempotency-Key: <c1b2@jtmerlin.example>" \-d "{\"channel\": \"email\",\"recipient_hash\": \"$RECIPIENT_HASH\",\"message_id\": \"<c1b2@jtmerlin.example>\",\"from_address\": \"alerts@jtmerlin.example\",\"sent_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"template_id\": \"new-device-alert-v2\",\"provider\": \"api\"}"Response:
{ "id": "com_01J...", "verify_code": "KX73-PQ9G", "expires_at": "2026-12-26T14:42:10Z", "sandbox": true }Standard library only (
hmac,urllib), no dependencies.samples/register.py """Register one sent email with Fromenance (Python 3.9+, standard library only, nothing to install).Environment:FROMENANCE_API_KEY fr_test_... from Settings, API keys (scope: register)FROMENANCE_TENANT_SECRET the recipient hashing secret from SettingsFROMENANCE_API_ORIGIN optional, defaults to https://api.fromenance.comThe recipient address is hashed here and never sent. This sample omits the content fingerprint, so theregistration verifies by code plus recipient; see the TypeScript fingerprint sample for the full pipeline."""import hashlibimport hmacimport jsonimport osimport reimport sysimport urllib.errorimport urllib.requestimport uuidfrom datetime import datetime, timezoneAPI = os.environ.get("FROMENANCE_API_ORIGIN", "https://api.fromenance.com").rstrip("/")def canonical_address(address: str) -> str:s = address.strip()m = re.search(r"<([^>]+)>", s)if m:s = m.group(1)s = s.strip().lower()if s.startswith("mailto:"):s = s[len("mailto:"):]return sdef recipient_hash(address: str, tenant_secret: str) -> str:digest = hmac.new(tenant_secret.encode("utf-8"), canonical_address(address).encode("utf-8"), hashlib.sha256)return "hmac-sha256:" + digest.hexdigest()def register(api_key: str, body: dict, idempotency_key: str) -> dict:data = json.dumps(body).encode("utf-8")req = urllib.request.Request(f"{API}/v1/communications",data=data,method="POST",headers={"Authorization": f"Bearer {api_key}","Content-Type": "application/json","Idempotency-Key": idempotency_key,},)try:with urllib.request.urlopen(req, timeout=15) as res:return json.loads(res.read().decode("utf-8"))except urllib.error.HTTPError as err:problem = json.loads(err.read().decode("utf-8"))raise RuntimeError(f"{problem.get('status')} {problem.get('title')}: {problem.get('detail', '')} "f"(request {problem.get('request_id')}) {problem.get('errors', '')}") from errdef footer(code: str, institution: str, verify_address: str, verify_page_url: str) -> str:page = re.sub(r"^https?://", "", verify_page_url)return (f"Not sure this email is from {institution}? Forward it to {verify_address}\n"f"or enter code {code} at {page}. Reference: {code}")def main() -> None:api_key = os.environ.get("FROMENANCE_API_KEY")tenant_secret = os.environ.get("FROMENANCE_TENANT_SECRET")if not api_key or not tenant_secret:sys.exit("Set FROMENANCE_API_KEY and FROMENANCE_TENANT_SECRET")message_id = f"<{uuid.uuid4()}@jtmerlin.example>"body = {"channel": "email","recipient_hash": recipient_hash("jane.doe@example.com", tenant_secret),"message_id": message_id,"from_address": "alerts@jtmerlin.example","sent_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),"link_domains": ["jtmerlin.example"],"template_id": "new-device-alert-v2","provider": "api",}registration = register(api_key, body, message_id)print(f"registered {registration['id']} with code {registration['verify_code']}")print(footer(registration["verify_code"], "JT Merlin Bank", "verify@jtmerlin.example", "https://jtmerlin.example/verify"))if __name__ == "__main__":main()Terminal window python3 samples/register.py -
Paste the footer into the message and send it to yourself.
Substitute the code from the response for
{{fromenance_code}}. The code appears twice on purpose: once in the sentence and once as a bare reference, so one survives client rewrapping and screenshot OCR.Not sure this email is from JT Merlin Bank? Forward it to verify@jtmerlin.exampleor enter code {{fromenance_code}} at jtmerlin.example/verify. Reference: {{fromenance_code}}With
KX73-PQ9Gsubstituted:Not sure this email is from JT Merlin Bank? Forward it to verify@jtmerlin.exampleor enter code KX73-PQ9G at jtmerlin.example/verify. Reference: KX73-PQ9GSend the email, with the footer, from
alerts@jtmerlin.exampleto the address you registered (jane.doe@example.comin the samples; use one you control). -
Forward it and read the reply.
From the recipient mailbox, forward the message to
verify@jtmerlin.example. Within seconds a reply arrives from your reply identity (jtmerlin@reply.fromenance.comuntil you set up branded replies). Its locked verdict block reads:Verified: this message matches a communication we registered and sent to you on September 24, 2026 at 2:42 PM UTC.
Because this is a sandbox key, the reply goes to your fraud contact address rather than to the forwarding mailbox. The submission also appears in the admin app under Submissions with rule
code+recipient+content(orcode+recipientwhen you registered without a fingerprint) and theverdict.createdevent fires on any webhook endpoint you have configured.
- The complete checklist with every request and response: Integration guide.
- Wire your real sending path: Registering communications and the adapter page for your ESP.
- Put the verify widget on your site: Verify page.
- Send verdicts to your SIEM: Webhooks.