Skip to content

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.

  1. Create an API key.

    In the admin app open Settings, API keys, and mint a key named quickstart with mode test and scope register. 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 Settings
    export FROMENANCE_API_ORIGIN=https://api.fromenance.com
  2. 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 status is verified:

    Terminal window
    curl -X POST https://api.fromenance.com/v1/domains/dom_.../check \
    -H "Authorization: Bearer $FROMENANCE_API_KEY"

    The domain gets the roles trust and sender by default. trust lets forwards that authenticate for it through; sender lets it appear as a registered From address.

  3. Create the redirect rule.

    Create the mailbox or group verify@jtmerlin.example and add a rule that redirects (not forwards) everything it receives to your tenant inbox, jtmerlin@verify.fromenance.com. The address is shown as verify_address on GET /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.

  4. 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.example with 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 until status is working:

    Terminal window
    curl https://api.fromenance.com/v1/domains/dom_.../forwarding-test \
    -H "Authorization: Bearer $FROMENANCE_API_KEY"

    A failed result names the rejection reason. The common one is auth_failed, which means the rule forwarded instead of redirecting.

  5. Register the first communication.

    One POST /v1/communications per 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.ts and recipient-hash.ts next 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.ts
  6. 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.example
    or enter code {{fromenance_code}} at jtmerlin.example/verify. Reference: {{fromenance_code}}

    With KX73-PQ9G substituted:

    Not sure this email is from JT Merlin Bank? Forward it to verify@jtmerlin.example
    or enter code KX73-PQ9G at jtmerlin.example/verify. Reference: KX73-PQ9G

    Send the email, with the footer, from alerts@jtmerlin.example to the address you registered (jane.doe@example.com in the samples; use one you control).

  7. 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.com until 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 (or code+recipient when you registered without a fingerprint) and the verdict.created event fires on any webhook endpoint you have configured.