# Add SafeBind to a publisher page that already runs TrustedForm or Jornaya LeadiD

**Who this is for.** You generate leads, your form pages already carry a consent-certification
script — TrustedForm, Jornaya LeadiD, or both — and you want SafeBind's Proof of Consent
created alongside them. This document is written to be handed to an AI coding assistant
together with your page code: it maps what your existing tags do to the SafeBind equivalent,
so the install is an addition, not a rework. The certifiers coexist; nothing here asks you
to remove what you run today.

**What this document is not.** It is a map, not the reference. The live install flow —
your key, your verified domains, a copy-paste snippet — is in the console at
`https://app.safebind.ai` (Install). The API spec is `https://api.safebind.ai/openapi.json`,
rendered at `https://safebind.ai/docs`. When this page and those disagree, they win.
TrustedForm, Jornaya, and LeadiD are named here only to map from integrations you already
have.

---

## Instructions for the coding assistant

You are likely reading this alongside an existing lead-funnel codebase. Do these in order:

1. **Find the existing certifier tags** so you can place SafeBind beside them:
   - TrustedForm: a script loaded from an ActiveProspect origin that writes hidden
     certificate fields (e.g. `xxTrustedFormCertUrl`) into the form.
   - Jornaya LeadiD: a per-campaign script plus a hidden token input (typically named
     `leadid_token` or `universal_leadid`).
   Leave them untouched.
2. **Add the SafeBind snippet on every page of the lead funnel** (head or body — site-wide
   is easiest and safest):

   ```html
   <script async src="https://safebind.ai/t.js?k=ck_live_…"></script>
   ```

   The `ck_live_…` value is your **site key**; the console's Install page gives you the
   snippet with it already filled in. It is not your API key. **Verify
   the domain first** (console step 1): verification is what gives records `verified`
   status from the first lead. A capture from a domain whose verification is still pending
   produces a **provisional** record — The Evidence is real and signed, but the record
   stays provisional **permanently**: verified-at-capture status is frozen and never backdated,
   buyer decisions on a provisional record degrade to `review` (and a buyer's rules
   may reject it outright), and verifying the domain later only sets a live
   `domain_verified_after_capture` overlay and makes **future** captures verified.
3. **Confirm the lead post carries the new fields** (they are injected automatically for a
   plain HTML `<form>` — see below), or wire the one SPA line if the form submits via
   JavaScript.
4. **Point test and staging pages at test evidence:** add `&test=1` to the snippet URL there.
5. That's the whole install. The snippet is the only way records are created; the API
   calls further down read and keep the records it created, and are optional.

---

## Concept map

| in your existing page code | SafeBind equivalent | what changes |
|---|---|---|
| The certifier script tag (TrustedForm's loader, or LeadiD's per-campaign script) | One line: `<script async src="https://safebind.ai/t.js?k=ck_live_…"></script>` | Same key on **every page of the funnel**, not per-campaign — the trace follows the consumer across pages under one record. If the script runs only on the consent page, earlier steps aren't recorded, and fields filled on earlier pages arrive looking *publisher-prefilled*, which weakens the evidence. (A single-page wizard needs only its one placement.) |
| Hidden certificate fields the script writes (`xxTrustedFormCertUrl`; LeadiD's `leadid_token` / `universal_leadid`) | Auto-added to plain HTML forms: `safebind_record_url` (the record URL your buyer checks) and `safebind_record_id` (the record reference alone, `<token>.<secret>`, for ID-keyed pipelines — the shape a LeadiD-style integration already expects). Forward them exactly as written: the part after the dot is the lead secret that lets your buyer read the record at bid time | Nothing to write yourself. There is no separate ping field: one pointer rides with the lead. |
| JavaScript-submitted forms (SPA/wizard) where your code appends the token to the payload | `payload.safebind_record_url = window.safebind?.recordUrl;` on submit — or `window.safebind?.recordId` for the reference alone (`<token>.<secret>`) | One line, same pattern your existing integration uses. |
| TrustedForm consent tagging (`data-tf-element-role` attributes) | `data-safebind="section"` / `"disclosure"` / `"submit"` — **or your existing `data-tf-element-role` tags, which work as-is** | Tags are optional: they point the recording at each offer's disclosure on multi-offer pages. Zero retagging if the page is already tagged. |
| LeadiD-style TCPA-disclosure flagging | Nothing to tag | The disclosure text, the consent control, and the affirmation are read from the session recording itself; tagging is an *option* for multi-offer pages, not a requirement. Disclosure prominence is **measured**, not scored from tags — worst proven WCAG contrast pair, smallest proven font size, and named cloaking findings — so a conspicuous disclosure needs nothing from you to prove itself. |
| Your lead/campaign reference on the record | `&xid=` on the snippet URL | Returned to your buyer as `record.external_id` so they can join the record to the lead it arrived with, and to you as a lookup key on `GET /v1/reporting?role=seller&external_id=`. Opaque to SafeBind. |
| Sandbox / test traffic | `&test=1` on the snippet URL | Every record the page creates is marked test evidence (`status.is_test: true` before the buyer bids, `record.is_test` in their report), so integration traffic can never be mistaken for real proof of consent. Test records are kept 7 days from capture, then deleted; saving one does not extend that. |
| Debugging the tag | `&sbdebug=1` on the snippet URL | Prints what the tag is doing to the browser console. |

One consolidation note: since January 2026 TrustedForm and Jornaya LeadiD are issued by the
same company — see `https://safebind.ai/jornaya-alternative` for that comparison. It changes
nothing in this document's mappings; both integrations keep working beside SafeBind.

## Before and after: a plain HTML form

Before — a form that already carries TrustedForm:

```html
<head>
  <!-- your existing TrustedForm loader stays exactly as it is -->
</head>
<form action="/submit-lead" method="post">
  <input name="first_name"> <input name="phone"> <input name="email">
  <label><input type="checkbox" name="tcpa_consent"> By checking this box I agree…</label>
  <button type="submit">Get my quote</button>
</form>
```

After — one line added to the page (every page of the funnel), nothing changed in the form:

```html
<head>
  <!-- your existing TrustedForm loader stays exactly as it is -->
  <script async src="https://safebind.ai/t.js?k=ck_live_YOUR_SITE_KEY"></script>
</head>
<form action="/submit-lead" method="post">
  <input name="first_name"> <input name="phone"> <input name="email">
  <label><input type="checkbox" name="tcpa_consent"> By checking this box I agree…</label>
  <button type="submit">Get my quote</button>
</form>
```

On submit, the post now also carries (the tag adds these hidden fields itself):

```text
safebind_record_url = https://api.safebind.ai/c/<token>.<secret>
safebind_record_id  = <token>.<secret>
```

Forward both to your buyer exactly as you forward `xxTrustedFormCertUrl` today, and put them in your ping-post spec. Send each value whole: the part after the dot is the lead secret. With it a buyer can read the record at once; without it the record's contents stay locked until about an hour after the session, too late for an auction. SafeBind never stores the secret, so only the lead carries it.

## Single-page apps (React / Next.js)

Load the tag once for the whole app, and read the values when the form submits:

```tsx
// app/layout.tsx (Next.js App Router)
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script src="https://safebind.ai/t.js?k=ck_live_YOUR_SITE_KEY" strategy="afterInteractive" />
      </body>
    </html>
  );
}
```

```tsx
// wherever the lead is posted
declare global {
  interface Window {
    // Empty strings until the record exists — send nothing rather than an empty value.
    safebind?: { recordUrl: string; recordId: string; status: string };
  }
}

async function submitLead(fields: Record<string, string>) {
  await fetch("/api/leads", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      ...fields,
      safebind_record_url: window.safebind?.recordUrl || undefined, // the URL your buyer checks
      safebind_record_id: window.safebind?.recordId || undefined // <token>.<secret>, for ID-keyed pipelines
    })
  });
}
```

Use the same field names your buyers expect; the ones above match what the tag writes into
plain HTML forms.

## Check the install

1. Open the funnel with `&sbdebug=1` added to the snippet URL (or on a staging copy): the
   browser console shows what the tag is doing.
2. Fill the form as a consumer would and submit. The post must carry `safebind_record_url`
   and `safebind_record_id` (your server logs, or the browser's network tab).
3. Open the `safebind_record_url` value in a browser: with no key it shows the record's
   four public facts.
4. The record appears in the console under Records, and from the API:
   `GET /v1/records` with your API key.
5. While testing, keep `&test=1` on the snippet URL so nothing you submit is sellable
   evidence; remove it for production.

## Done when

- [ ] The domain is verified in the console (before the first real lead).
- [ ] The snippet is on **every** page of the funnel, with your site key, beside the existing
      certifier tags (which still work).
- [ ] Test leads carry `safebind_record_url` and `safebind_record_id` (plain forms automatically,
      SPAs through `window.safebind`), and your lead post forwards both to buyers.
- [ ] Staging pages use `&test=1`; production pages do not.
- [ ] If you read your records from code: an **API key** (not the site key) lives only on
      your server, and `GET /v1/records` returns them.

## Keys — how SafeBind's differ

| | TrustedForm Certify Web SDK | SafeBind |
|---|---|---|
| Key in the page tag | None: the script is configured by variables (`field`, `sandbox`, `invert_field_sensitivity`) and names no account | Your **site key** (`ck_live_…`), already in the snippet URL. It ties every record to your account from the first capture, and it is ingest-only: it sits in your page source by design, so it can capture consent and nothing more: every API call refuses it (`403`), and on a record URL it unlocks nothing beyond the anonymous view. |
| Test traffic | `&sandbox=` on the script; TrustedForm sandbox certificates cannot be claimed | `&test=1` on the snippet URL; test records are marked `is_test`, can never be filed as proof, and are deleted 7 days after capture |
| Key for the API | Your ActiveProspect account API key (HTTP Basic auth, username `API`), shared across every ActiveProspect product | Your **API key** (`sk_live_…`) — a different key from the site key. Send it however you like: `Authorization: Bearer sk_live_…`, HTTP Basic auth with the key as the password (`-u API:sk_live_…`, as you send TrustedForm's key today), or an `x-api-key` header. Create as many as you need, each named and each limited to the capabilities you choose. Never put an API key in a page. |

Both kinds are created and revoked in the console (API keys). Rotating one key leaves the
others untouched; a revoked key can keep working for up to about 10 minutes while caches
expire, so switch before you revoke.

## What the snippet creates

A record per consumer session: the session recording, the disclosure as displayed, the
consent control and the second it was affirmed, the parties named, the network and device
signals — signed, independently timestamped, and addressed by the `safebind_record_url` that
submits with your lead. The URL itself is meaningful: opened with no credentials it shows a
court four non-sensitive facts; your buyer's reads are a `POST` to the same address, and
the masked, PII-free session projection is watched in the SafeBind console.

Records are created only by the snippet, from what it witnessed on your page. There is no
API for submitting evidence recorded elsewhere.

## Reading and keeping your own records (API)

Every call takes your **API key** (`sk_live_…`) from the console's API keys page — as a
Bearer token, as HTTP Basic credentials, or in an `x-api-key` header, whichever suits your
code. Base URL: `https://api.safebind.ai`.

| call | what it returns |
|---|---|
| `GET /v1/records` | Your records, newest first: `id`, `status`, `publisher_domain_id`, `signature_status`, `created_at`. Keyset-paginated; keep following `next_cursor` until it is `null`. |
| `GET /v1/records/{recordId}` | One record's status, signature, and Merkle root. |
| `GET /v1/records/{recordId}/screening` | Your own Lead screening result for the record: `screening.recorded` (decided at creation, under the rules active then), `screening.current` and `screening.changed` (what your current rules decide), plus the capture-time risk `fraud_band` with the `fraud_reasons` that set it. Advisory — the band flags a lead, it does not hold one. |
| `GET /v1/records/{recordId}/legal-export` | The legal-export bundle: signed record, RFC-3161 timestamp token, chain of custody, and the facts of the capture. Deliberately contains **no** lead-screening analysis — the exhibit states what happened; scoring is a separate question. It carries `exportReceipt`, a receipt SafeBind signs naming your company as the holder, and on records captured from September 19, 2026 the partner list shows as `sealedPartnerList` (its fingerprints) rather than the page. |
| `GET /v1/records/{recordId}/evidence` | The raw Evidence you own, as signed. Consumer PII is withheld (`recordingWithheld: true`, with the withheld parts named in `withheldLeaves`) once your access window has closed **or** the capturing domain is no longer verified. `409` means the bytes were archived to WORM storage — retrievable as a paid service, never silently deleted; `410` means the evidence was erased. |
| `POST /v1/records/{recordId}/save` | **Keep** one of your own records for five years (needs a key with the `create` capability; billed per record). Idempotent: `firstSave: false` on a repeat, nothing charged. The console's **Keep 5 yrs** button and account-wide auto-keep do the same without code. |
| `GET /v1/reporting?role=seller` | Bulk export of the record-level analysis of your records, by date range, record ids, or your `external_id`. Free. |

Everything else — verifying domains, creating and revoking keys, your Lead screening rules —
lives in the console at `https://app.safebind.ai`, not the API. There are no webhooks: poll
`GET /v1/records` or the reporting endpoint.

## The clocks your buyers live on (worth knowing as the seller)

- The buyer's self-serve window to save a record (`savable_until`) is **90 days from
  capture**, fixed once the record is signed — 7 days when no consent submission was
  detected.
- Evidence nobody keeps is destroyed **one year from capture**. A buyer's save keeps their
  copy for **five years**, and you can keep your own for five years the same way (Keep,
  auto-keep, or `POST /v1/records/{recordId}/save`). The durations themselves are not
  configurable.

Point your buyers at the buyer-side companion to this document:
`https://safebind.ai/buyer-migration.md`.

---

*"TrustedForm", "Jornaya" and "LeadiD" are trademarks of ActiveProspect, Inc. SafeBind is
not affiliated with, endorsed by, or sponsored by that company; names are used for
identification only, to map from integrations a publisher already runs. Statements about
those products describe the typical shape of existing integration code, not their current
behavior or pricing.*
