Attestio
Product

The evidence layer for every lead

Capture how consent was given, seal it so it can't be altered, and score the lead before anyone calls.

Product overview
Capture & certify
Consent captureRead and store the exact wording shown RecordsFormats, retention and portability Session replayWatch the submission, event by event
Inspect & decide
Lead scoringA score and the reasons behind it Double opt-inConfirm consent a second time Migrate to AttestioMoving from TrustedForm, step by step
Compare

See the differences yourself

Feature-by-feature breakdowns against the platforms most lead buyers are already running.

All comparisons
Attestio vs.
vs TrustedFormWhat it does, costs and misses vs VerfiRetention, replay and pricing vs JornayaFeatures, pricing and fit vs ExpressConsentWhich consent platform wins
Buyer's guides
TrustedForm alternativesA complete buyer's guide TrustedForm vs JornayaFeature and pricing comparison Attestio pricingPublished rates, no sales call
Industries

Compliance, vertical by vertical

What the rules actually require in your industry, and who carries the liability when a claim lands.

All industries
Financial & insurance
Financial services Debt relief Insurance Mortgage
Home, legal & other
Solar Home services Legal Education SaaS Marketing agencies Direct to consumer
Laws & compliance

The rules, explained plainly

Statutes, case law and practical checklists — written for people who have to comply, not lawyers.

Complete TCPA guide
TCPA
What is the TCPA? Compliance checklist Regulations & FCC rules Violations & penalties State mini-TCPA laws TCPA and AI Penalty calculator
Consent
Consent hub Expressed consent Express written consent Written consent forms Consent language examples Revoking consent One-to-one consent rule
Other laws
FCRA Mortgage trigger leads CIPA Session replay lawsuits FDCPA TSR Run a compliance audit
Developers

One script tag to install

Async, light, and framework-free. Store records by API and keep the ID against your own records.

Developer docs
Reference
How inspection worksThe publisher-to-buyer flow, explained API referenceEndpoints, auth and payloadsForm Field Annotation GuideTag and mask your form fields Integrations overviewWebhooks, Zapier and CRMs System statusUptime and incident history
Integration guides
Salesforce Zapier VanillaSoft Support center
Pricing
Product overview Consent capture Records Session replay Lead scoring Double opt-in Migrate to Attestio
All comparisons Attestio vs TrustedForm Attestio vs Verfi Attestio vs Jornaya Attestio vs ExpressConsent TrustedForm alternatives TrustedForm vs Jornaya
All industries Financial services Debt relief Insurance Mortgage Solar Home services Legal Education SaaS Marketing agencies Direct to consumer
TCPA
Complete TCPA guide What is the TCPA? Compliance checklist Regulations & FCC rules Violations & penalties State mini-TCPA laws TCPA and AI Penalty calculator Run a compliance audit
Consent
Consent hub Expressed consent Express written consent Written consent forms Consent language examples Revoking consent One-to-one consent rule
Other laws
FCRA Mortgage trigger leads CIPA Session replay lawsuits FDCPA TSR
Developer docs How inspection works API referenceForm Field Annotation Guide Integrations overview Salesforce Zapier VanillaSoft Support center System status
Pricing Log in
  1. Home
  2. Developers
API reference

Inspect API

One call to confirm a record is genuine — and, in the same request, return its metadata, the consent language the applicant saw, and a privacy-preserving match against the buyer’s own PII.

Status
Live
Version
1.0
Last updated
October 29, 2026
Base URL
api.attestio.com/v1
Authentication
API key

Make a test call with your API key — or as your signed-in account — in the Attestio Testing Tool and see the exact response.

Attestio Testing ToolDownload PDF
API reference

Store API

One call to take possession of a record — confirming it and writing a permanent, timestamped entry showing that your account, and no one else’s, stored it.

Status
Live
Version
1.0
Last updated
October 26, 2026
Base URL
api.attestio.com/v1
Authentication
API key

Make a test call with your API key — or as your signed-in account — in the Attestio Testing Tool and see the exact response.

Attestio Testing ToolDownload PDF
API reference

Integrate API

One call to subscribe your CRM or lead system to Attestio events, so the proof behind every lead you store arrives where you work — automatically, and without personal data.

Status
Live
Version
1.0
Last updated
October 13, 2026
Base URL
api.attestio.com/v1
Authentication
API key

Coming soon: make a test call with your own API key in the Attestio Testing Tool and see the exact response.

Attestio Testing ToolDownload PDF
Publisher guide

Form Field Annotation Guide

Add one HTML attribute to each field that holds contact details, and one to anything that should never be seen. Tagged fields are mapped exactly, so advertisers can confirm each lead.

Status
Draft
Version
1.1
Audience
Publishers and developers
Last updated
October 28, 2026

Run your form through the Attestio Testing Tool to confirm every field is detected and masked as expected.

Attestio Testing ToolDownload PDF
    API reference
    Inspect APIStore APIIntegrate API
    Publisher guides
    Form Field Annotation Guide
    Read me

    One API key works for Inspect, Store and Integrate. Every field documented here is backed by data Attestio captures and stores today. Personal data you send is only ever matched as a hash, and the form’s values are never returned; the visitor’s IP address is returned in plain text.

    1Overview & design principle

    The Inspect API answers one question a lead buyer asks at the moment of purchase or onboarding: “Is this record real, and what do you know about it?”

    Its guiding principle is one call, everything you need. A buyer never has to chain requests — they name the record and optionally pass hashed PII to be matched, all in a single POST. The response always carries everything: the record (its ID, issue time, expiration, and when you stored it), the account and form that produced the record, a true/false per PII field, and the session — the page, where the visitor was, their network and device, the consent and how it was shown, how they interacted and how the form was filled — so the buyer can decide from one round trip.

    In one call

    Record (ID, issued, expiration, stored) + account + form + PII match + session (timing, page, geo, network, device, consent, interaction, behavior) — always returned together. No sections to choose, no second request, no polling.

    2Base URL & versioning

    All requests use HTTPS and are versioned in the path.

    Base URL
    https://api.attestio.com/v1

    The API is served on its own api. subdomain, separate from the app and the tracker.

    3Authentication

    Every request authenticates with an API key, sent as a bearer token. A key is created once, shown once, and pasted into the buyer’s CRM or integration tool. It never expires and can be revoked at any time — revocation takes effect on the next request. Attestio stores only a hash of the key, and records who created it and when it was last used.

    Create your key in the dashboard under Settings → API keys; the same key works for Inspect, Store and Integrate. A key acts as the account that created it, and can inspect any record by its record ID (the ID is what proves you were given the record). An unknown record ID returns 404.

    Header
    Authorization: Bearer atk_live_7f3aQm2Xk9…

    A session token from a signed-in dashboard user is also accepted in the same header. A session token can inspect the records that account can open in the dashboard; for integrations, always use an API key.

    Testing without writing code

    Two ways to make a first call before wiring up an integration:

    • Attestio Testing Tool. In the dashboard, open Account settings → API Functions, click Open Attestio Testing Tool and choose the Advertiser Testing Tools tab. Pick Inspect, enter a record ID and, optionally, an email, phone or full name to match (each value is hashed in your browser and sent as email_hash, phone_hash or name_hash), then press Send request. Paste your API key to call the API with it, or leave the key blank to call as your signed-in account. The status, timing and JSON response appear alongside, and the cURL tab gives the equivalent command to copy for a developer.
    • Session token. While signed in to the dashboard, the value of the attestio_session cookie is a valid bearer token for the same endpoint. Useful for a quick check from a terminal or API client without minting a key; it expires with the session, so integrations should always use an API key.
    Case

    The API key, JSON field names and 22-character record IDs are case-sensitive (base64 — A and a differ). PII values are not case-sensitive — emails, names, addresses and postal codes are normalised before hashing, and phone numbers are compared digit-only with the US country code ignored. A hex hash may be any case; a base64 hash is case-sensitive.

    A missing or invalid token returns 401. An unknown record returns 404; with a session token, so does a record that account cannot open in the dashboard — identical on purpose, so a token cannot be used to probe which records exist.

    4The Inspect endpoint

    POST/v1/inspect

    Request body

    FieldTypeDescription
    record_idstringRequired
    The record to inspect: its 22-character ID, e.g. A7guruOFQQKvUK-vHAGx4w.
    email_hash
    phone_hash
    name_hash
    address_hash
    postal_code_hash
    date_of_birth_hash
    stringOptional
    Values to match against the captured submission, one field per value you want checked. Every value is a hash — SHA-256 of the normalised value, no salt — so no plaintext ever crosses the wire (see §5, Hashed PII). Send it as hex (64 characters, any letter case) or base64 (43 or 44 characters, standard or URL-safe, with or without padding); the length tells us which. A plain value is rejected with 400. Masked and unmasked fields are inspected the same way.

    SSN, card number and income are not inspectable: ssn_hash, card_hash and income_hash return 400.

    Response fields

    Always returned complete, in this order. Every timestamp is a *_timestamp field in UTC, ISO 8601 with milliseconds (e.g. 2026-10-20T22:05:34.456Z).

    FieldTypeDescription
    record.idstringThe record’s 22-character ID. An unknown or invalid ID returns an error instead, with no record.
    record.issued_timestampstringWhen the record was issued.
    record.expiration_timestampstringUntil when the record is kept for you. While your account stores it (Store API): when your store ends. Otherwise: issued_timestamp + 7 days. It never reflects other accounts’ stores.
    record.stored_timestampstringPresent only while your account stores the record: when that store began. Absent otherwise — including after your store has ended.
    account.id
    account.name
    string | nullThe account that generated the record, and its name (the company name, else the account owner’s name, else the domain of the owner’s email). Never a full email address.
    form.id
    form.name
    string | nullThe form that generated the record, and its name (Forms and Domains).
    pii_match.*booleanOne per field you sent: true when it matches what was captured, false when it does not — or when that field was not captured on this record (it cannot be proven). The stored value is never returned. Absent when you sent no PII.
    session.begin_timestamp
    session.end_timestamp
    string | nullWhen the visitor’s session began and ended (ISO 8601, milliseconds).
    session.durationnumber | nullSession length in seconds, e.g. 202.333.
    session.agenumber | nullSeconds since the session ended, at the moment of your call — how fresh the lead is. Grows with every call.
    session.page.urlstring | nullThe page the form was submitted on — origin and path only. The query string is left out because funnels often carry personal data in it.
    session.page.referrerstring | nullThe page the visitor came from, origin and path only; null when they came directly.
    session.page.framestringWhere the form ran on the page: "top-level", "iframe-same-origin" or "iframe-cross-origin" (a form embedded from another website).
    session.geo.city
    session.geo.region
    session.geo.country
    string | nullWhere the visitor was, from their IP address, e.g. "Sturgis", "MI", "US"; null when it could not be placed.
    session.network.ipstring | nullThe visitor’s IP address as captured, in plain text (not hashed), e.g. "203.0.113.24" or an IPv6 address.
    session.network.ip_network
    session.network.vpn_detected
    string | null
    boolean | null
    What kind of network the IP belongs to — "unrecognized" (an ordinary connection), "vpn", "tor" or "hosting" (a datacenter) — and whether a VPN or proxy was detected. null when not looked up.
    session.device.typestring | null"mobile", "tablet" or "desktop".
    session.device.browser
    session.device.os
    string | nullThe visitor’s browser and operating system, e.g. "Facebook 568" (an in-app browser) and "iOS 26.6".
    session.device.user_agentstring | nullThe visitor’s browser user agent, as sent when the form was submitted.
    session.device.viewport_width
    session.device.viewport_height
    number | nullThe size of the visitor’s browser window in pixels, e.g. 390 × 844; each null when not captured.
    session.device.headless_browserboolean | nulltrue when the browser showed signs of being automated (headless).
    session.consent.textstring | nullThe consent language captured on the page, or null when none was captured.
    session.consent.detectedbooleantrue when a consent disclosure was found on the page the visitor saw.
    session.consent.font_size_px
    session.consent.contrast_ratio
    number | nullHow the disclosure rendered: its font size in pixels, and its text-to-background contrast (WCAG ratio, 1–21). null until measured.
    session.consent.placementstring | nullWhere the disclosure sat when the form was shown, as the record’s Placement card says: "in_viewport", "partly_in_viewport" or "out_of_viewport"; null until measured.
    session.consent.above_submitboolean | nulltrue when the disclosure sat above the submit button.
    session.interactionobjectRaw counts of what the visitor did: events_recorded (events in the session recording), mouse_events, mouse_clicks, keypresses and cursor_distance_px. Each null when not captured.
    session.behavior.verdictstringOur reading of how the form was filled — the record’s form-behavior tag: "normal", "prefilled" (a field arrived filled), "autofilled" (the browser filled most of the form) or "possible_bot" (a headless browser in under 10 seconds).
    session.behavior.input_methodobjectHow the form was filled, in percent: prefilled, manual, autofill, paste.
    session.behavior.typing_speed_wpmnumber | nullTyping speed in words per minute; null on embedded forms.
    session.behavior.fields_completed
    session.behavior.fields_total
    session.behavior.backtracking
    number
    number
    boolean | null
    How many fields were filled out of how many, and whether the visitor went back to an earlier field.

    Errors

    Every error is one error object with a message saying what went wrong; the HTTP status tells the kind of failure. No record is returned with an error. A request fails for one reason, so one error is returned; if a case ever needs several, they come as an errors array of the same objects.

    404 Not Found · application/json
    {
      "error": {
        "message": "Invalid Attestio record ID."
      }
    }
    StatusWhen
    400The record_id is not a valid record ID: Invalid Attestio record ID.
    400Malformed request — the message says what to fix: a missing record_id; a plain value instead of a hash; a hash of the wrong length; an unsupported field such as ssn_hash; the same field sent twice.
    400The body is not valid JSON.
    401Missing, invalid or revoked API key.
    403The key is limited to the Store API (an older single-purpose key).
    404No record with that ID — the same message as a malformed ID (Invalid Attestio record ID.), so IDs cannot be probed.
    IDs

    Every ID is unpadded base64url: record IDs are 22 characters (128 bits); account and form IDs are shorter.

    5Hashed PII

    To check whether an email, a name, or any other field matches what the record captured, you send a hash of the value — never the value itself. For security, raw PII must never cross the wire. The hash is a plain SHA-256 of the normalised value (no salt) — the same hash Attestio’s tracker computes for a masked field at capture — and the server compares it with what it captured. It follows two steps: hash the value on your own machine, then call the API with that hash.

    Why

    The raw value stays on your machine — hash it in your browser (the Attestio Testing Tool), your CRM, or a terminal. Because it is the same hash the tracker made when the publisher masked the field, even a value Attestio never stored in the clear can be inspected. Every *_hash value is checked this way — a raw value is rejected with 400.

    Step 1 — Hash your email

    Run this in any terminal. It prints the hash of the email; the email itself is never sent anywhere. Replace the email with the one you want to check.

    Shell — prints the hash
    printf %s "you@example.com" | tr -d ' ' | tr '[:upper:]' '[:lower:]' | shasum -a 256 | awk '{print $1}'

    Prefer base64? The same hash, written shorter (44 characters) — either form is accepted:

    Shell — the same hash as base64
    printf %s "you@example.com" | tr -d ' ' | tr '[:upper:]' '[:lower:]' | openssl dgst -sha256 -binary | base64
    Any format works

    Send the SHA-256 hash in whichever form your tool gives you — all of these are accepted and give the same result: hex lowercase (64 characters), hex UPPERCASE (64), base64 with padding (44, ends in =), base64 without padding (43) or base64 URL-safe (43, uses - and _). You never say which one you used — the length tells us. Examples of each are under Accepted hash formats below.

    Add a field

    Want to match the name too? Hash it the same way (run Step 1 with the name) and add it to the request, separated by a comma, like this: "email_hash": "<hash>", "name_hash": "<hash>".

    Step 2 — Call the Inspect API

    Paste the hash from Step 1 into email_hash, set your record ID, and send the request. Your API key is used only as the bearer token that authenticates the call — it is not part of the hash, so any key on the account gives the same result. Non-developers can simply paste a key into the Authorization header.

    cURL — call the endpoint
    curl -X POST https://api.attestio.com/v1/inspect \
      -H "Authorization: Bearer atk_live_7f3aQm2Xk9…" \
      -H "Content-Type: application/json" \
      -d '{
        "record_id": "A7guruOFQQKvUK-vHAGx4w",
        "email_hash": "PASTE_YOUR_HASH"
      }'

    Step 3 — Read the response

    The call always returns the complete response (field by field in §4), with true or false per PII field you sent. The stored value is never returned. For example:

    200 OK · application/json
    {
      "record": {
        "id": "A7guruOFQQKvUK-vHAGx4w",
        "issued_timestamp": "2026-10-20T22:05:35.210Z",
        "expiration_timestamp": "2026-10-27T22:05:35.210Z"
      },
      "account": { "id": "obLD1OX2obLD1OX2", "name": "Affiliate A" },
      "form": { "id": "nXj8yUJWTzoqLcir", "name": "Long Form" },
      "pii_match": { "email": true },
      "session": {
        "begin_timestamp": "2026-10-20T22:02:12.123Z",
        "end_timestamp": "2026-10-20T22:05:34.456Z",
        "duration": 202.333,
        "age": 65,
        "page": {
          "url": "https://quotes.example.com/long-form/",
          "referrer": "https://www.google.com/",
          "frame": "top-level"
        },
        "geo": {
          "city": "Atlanta",
          "region": "GA",
          "country": "US"
        },
        "network": {
          "ip": "203.0.113.24",
          "ip_network": "unrecognized",
          "vpn_detected": false
        },
        "device": {
          "type": "desktop",
          "browser": "Chrome 155",
          "os": "macOS",
          "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36",
          "viewport_width": 1512,
          "viewport_height": 827,
          "headless_browser": false
        },
        "consent": {
          "text": "By clicking Submit, I agree to be contacted…",
          "detected": true,
          "font_size_px": 12,
          "contrast_ratio": 7.21,
          "placement": "in_viewport",
          "above_submit": true
        },
        "interaction": {
          "events_recorded": 412,
          "mouse_events": 14,
          "mouse_clicks": 10,
          "keypresses": 22,
          "cursor_distance_px": 7517
        },
        "behavior": {
          "verdict": "normal",
          "input_method": { "prefilled": 0, "manual": 60, "autofill": 40, "paste": 0 },
          "typing_speed_wpm": 38,
          "fields_completed": 6,
          "fields_total": 6,
          "backtracking": false
        }
      }
    }

    Here your account does not store the record, so expiration_timestamp is 7 days after it was issued and there is no stored_timestamp. While your account stores it, expiration_timestamp is when your store ends instead, and stored_timestamp says when that store began:

    record — stored by your account
    "record": {
      "id": "A7guruOFQQKvUK-vHAGx4w",
      "issued_timestamp": "2026-10-20T22:05:35.210Z",
      "expiration_timestamp": "2031-10-21T22:10:00.000Z",
      "stored_timestamp": "2026-10-20T22:10:00.000Z"
    }

    The recipe

    Every hash is computed the same way. The Attestio Testing Tool and any client must match it exactly:

    Recipe
    hash = SHA256( normalize(field, value) )   # send as hex (64 chars, any case) or base64 (43/44 chars), no salt

    How to use it. For each field you want matched:

    • 1. Normalise the value with the field’s rule from the table below.
    • 2. SHA-256 the normalised value and write the digest as hex (64 characters, any letter case) or base64 (43 or 44 characters, standard or URL-safe) — whichever your tools produce. Do not add a salt or your API key — the key authenticates in the Authorization header only. (Attestio stores masked fields as this same hash, protected server-side with a secret pepper, which is why the hash you send must be the plain one.)
    • 3. Send it as "<field>_hash": "<hash>" — e.g. "email_hash" — one entry per field.

    Accepted hash formats

    The same hash can be written several ways; all of these are accepted and give the same result. This is SHA-256("jane@example.com") — send whichever your tools produce:

    FormatLengthExample
    Hex, lowercase648c87b489ce35cf2e2f39f80e282cb2e804932a56a213983eeeb428407d43b52d
    Hex, UPPERCASE648C87B489CE35CF2E2F39F80E282CB2E804932A56A213983EEEB428407D43B52D
    Base64, with padding44jIe0ic41zy4vOfgOKCyy6ASTKlaiE5g+7rQoQH1DtS0=
    Base64, without padding43jIe0ic41zy4vOfgOKCyy6ASTKlaiE5g+7rQoQH1DtS0
    Base64, URL-safe43jIe0ic41zy4vOfgOKCyy6ASTKlaiE5g-7rQoQH1DtS0

    We tell them apart by length (64 = hex, 43/44 = base64), so you never say which one you used. Anything else — including the plain value — is rejected with 400.

    Normalisation per field

    Apply the field’s rule to the value before hashing, so trivially-different spellings still match. Both the caller and the server use these exact rules.

    FieldNormalise the value to…
    emailtrim, then lowercase. "Jane@Example.com" → "jane@example.com"
    phonedigits only; drop a leading 1 country code if 11 digits. "+1 (555) 123-4567" → "5551234567"
    namelowercase; letters and spaces only (punctuation → space); collapse spaces. "Jane M. Doe" → "jane m doe"
    addresslowercase; alphanumerics and spaces only; expand street/avenue/road/drive/boulevard to st/ave/rd/dr/blvd; drop unit designators (apt, unit, suite…) and their number; collapse spaces. "123 Main Street, Apt 4" → "123 main st"
    postal_codealphanumerics only, uppercase, first 5. "33101-2210" → "33101"
    date_of_birthdigits only, in the captured order. "1990-02-01" → "19900201"; masked by default — still inspectable via the stored hash
    Masked fields

    Date of birth is masked at capture by default, and a publisher can mask any other category per form. Attestio never stores a masked field’s raw value — only the hash of this same recipe — so masked and unmasked fields are inspected identically: true or false. A field the record never captured returns false. SSN, card number and income can be masked but are not inspectable. The Attestio Testing Tool runs the whole recipe for you: type the raw value and it normalises and hashes in your browser, sending only the hash.

    Next →Store API

    1Overview & the store model

    A record reaches you one of two ways: a publisher issues it to you, or someone shares it with you. Before you act on it, you inspect it — confirm it is genuine, that the consent was shown, and that its details match your own data on the lead. Once you are satisfied it is real, you store it.

    Storing is the deliberate act of taking possession. Its guiding principle is inspect to trust, store to keep. A single POST confirms the record and writes an entry showing that your account stored this record, at this moment, kept for as long as you choose — or your account’s default period (5 years unless your account changed it). That entry is the record’s chain of custody.

    Inspect vs. store

    Inspect answers “is this record real?” and reads its data (inspecting another account’s record also lists it in your account until the record is 90 days old). Store says “I am keeping this one” — it notes who stored it and returns the record with its stored time, without re-sending the data you already read on inspect. Inspect as many times as you like; store when you have decided to use the lead.

    2Base URL & versioning

    All requests use HTTPS and are versioned in the path. Breaking changes ship under a new major version; additive fields do not.

    Base URL
    https://api.attestio.com/v1

    The Store API shares the Inspect API’s host and key — it is served on the same api. subdomain, separate from the app and the tracker.

    3Authentication

    Every request authenticates with an API key, sent as a bearer token — the same key you use for Inspect. A key is created once, shown once, and pasted into the buyer’s CRM or integration tool. It never expires and can be revoked at any time — revocation takes effect on the next request. Attestio stores only a hash of the key, and records who created it and when it was last used.

    Create your key in the dashboard under Settings → API keys. A key acts as the account that created it: every store is logged against that account, so always use a key from your own account.

    Any account holding a record’s ID may store it — the ID is what proves you were given the record. An unknown ID returns 404, so IDs cannot be probed. Storing is non-exclusive — any number of accounts may store the same record, and every store is logged separately.

    Header
    Authorization: Bearer atk_live_7f3aQm2Xk9…

    A signed session token (the JWT Attestio issues at login) is also accepted in the same header, so a record can be stored from an authenticated session without creating a key.

    A missing or invalid token returns 401. A key limited to the Inspect API (an older single-purpose key) returns 403. An unknown record returns 404, so tokens cannot be used to probe for which records exist. A malformed body returns 400. Every error is one error object with a message (see §4).

    4The Store endpoint

    POST/v1/store

    Storing takes the record to store and, optionally, how long to keep it: expiration_in_days or expiration_timestamp — never both. Send neither and your account’s default store period applies (5 years for a new account; change it in Settings → API keys). Any new value replaces the old one, longer or shorter. There are no sections to select — reading the record’s data (metadata, consent, the captured fields, PII matching) is the Inspect API’s job, and you do that before you store. Store does one thing: note that you have taken possession.

    Request body

    FieldTypeDescription
    record_idstringRequired
    The record to store: its 22-character ID, e.g. fyqJkSSJRNub0RxVxe8Wdw.
    expiration_in_daysintegerOptional
    How many days from now your store keeps the record: a whole number from 0 to 3653 (10 years). 0 ends your store.
    expiration_timestampstringOptional
    The date until which your store keeps the record — a date only, no time — at most 10 years from now. US order (month, day, year): 01-01-2028 or 01/01/2028; year first 2028-01-01 also works. The record is kept to the end of that day in UTC, which the response shows as 2028-01-01T23:59:59.999Z. A date in the past ends your store (it is saved as now).
    Read on inspect, store to keep

    The lead’s data — metadata, consent language, and the captured fields — is returned by the Inspect API, which you call first. Store intentionally does not re-send it; its response is a confirmation and a receipt, not a second copy of the data.

    Call it with cURL

    Set your record ID and send the request. Your API key is the bearer token that authenticates the call. Non-developers can simply paste a key into the Authorization header.

    cURL — store a record
    curl -X POST https://api.attestio.com/v1/store \
      -H "Authorization: Bearer atk_live_7f3aQm2Xk9…" \
      -H "Content-Type: application/json" \
      -d '{
        "record_id": "fyqJkSSJRNub0RxVxe8Wdw",
        "expiration_in_days": 365
      }'

    Or until a date — or send neither to use your account’s default period:

    Request body — until a date (US order: month-day-year)
    {
      "record_id": "fyqJkSSJRNub0RxVxe8Wdw",
      "expiration_timestamp": "10-10-2030"
    }
    Request bodyWhat happens
    { "record_id": "…" }Kept for your account’s default period. If your store is already active, nothing changes.
    "expiration_in_days": 0Ends your store now — the signal that you no longer need the record kept.
    "expiration_timestamp": "10-10-2030" (or "2030-10-10")Kept until the end of October 10, 2030 (UTC): 2030-10-10T23:59:59.999Z. US order is month first, so 03-04-2030 is March 4.
    a time, e.g. "2030-10-10T00:00:00.000Z"Rejected with 400: send a date only.
    a past date, e.g. "01-01-2020"Ends your store now (saved as now, not the past date).
    both fieldsRejected with 400: send one, not both.

    What it returns

    The call returns the record object — the same one the Inspect API returns — now with stored_timestamp. Its presence is your confirmation that the store went through. It does not re-send the record’s data; that is read on inspect. Every timestamp is a *_timestamp field in UTC, ISO 8601 with milliseconds.

    200 OK · application/json
    {
      "record": {
        "id": "fyqJkSSJRNub0RxVxe8Wdw",
        "issued_timestamp": "2026-08-26T20:43:25.123Z",
        "expiration_timestamp": "2031-10-07T14:12:09.481Z",
        "stored_timestamp": "2026-10-06T14:12:09.481Z"
      }
    }

    When the call ends your store ("expiration_in_days": 0 or a past date in expiration_timestamp), the record shows the store that just ended: expiration_timestamp is the moment it ended. Inspect then treats the record as not stored. If you had no store to end, the record comes back without stored_timestamp.

    200 OK — store ended
    {
      "record": {
        "id": "fyqJkSSJRNub0RxVxe8Wdw",
        "issued_timestamp": "2026-08-26T20:43:25.123Z",
        "expiration_timestamp": "2026-11-02T09:30:00.000Z",
        "stored_timestamp": "2026-10-06T14:12:09.481Z"
      }
    }

    Errors

    Every error is one error object with a message saying what went wrong; the HTTP status tells the kind of failure. No record is returned with an error. A request fails for one reason, so one error is returned; if a case ever needs several, they come as an errors array of the same objects.

    404 Not Found · application/json
    {
      "error": {
        "message": "Invalid Attestio record ID."
      }
    }
    StatusWhen
    400The record_id is not a valid record ID: Invalid Attestio record ID.
    400Malformed request — the message says what to fix: a missing record_id, or both expiration_timestamp and expiration_in_days sent (Send expiration_timestamp or expiration_in_days, not both.).
    400expiration_in_days is not a whole number from 0 to 3653, or expiration_timestamp is not a date (10-26-2031, 10/26/2031 or 2031-10-26), includes a time, is not a real date (e.g. 13-01-2031), or is more than 10 years ahead.
    400The body is not valid JSON.
    401Missing, invalid or revoked API key.
    403The key is limited to the Inspect API (an older single-purpose key).
    404No record with that ID — the same message as a malformed ID (Invalid Attestio record ID.), so IDs cannot be probed.
    500The store could not be written. Nothing was stored; it is safe to retry.

    5The store receipt & chain of custody

    Every successful store writes one row to the record’s chain of custody: which account stored which record, and when. That row is the point of the endpoint — it is what the account, and if it ever comes to it a court, reads as “who took possession of this evidence.” The record in the response reflects that row: stored_timestamp is when it was written and expiration_timestamp is until when it keeps the record for you.

    FieldTypeDescription
    record.idstringThe record’s 22-character ID.
    record.issued_timestampstringWhen the record was issued.
    record.expiration_timestampstringUntil when the record and its recording are kept for you: the expiry you sent, else stored_timestamp + your account’s default period. About now when the call ended your store.
    record.stored_timestampstringWhen the store this call left in place began (UTC, ISO 8601 with milliseconds). Absent only when you asked to end a store you did not have.

    Storing is idempotent: if your account has already stored this record, a repeat call returns the same stored_timestamp rather than writing a second row. Sending a new expiration_in_days or expiration_timestamp replaces your expiry — longer or shorter. Once your store has ended, storing again starts a new store: stored_timestamp is that new time. Every store and every end is logged, so the full history stays in the chain of custody.

    Retention

    A store is kept for your account’s default period — 5 years (1827 days) for a new account, changeable in Settings → API keys from 1 day to 10 years — or for the expiry you send, up to 10 years. A record you only inspected (not stored) stays in your account’s list until it is 90 days old, then drops out.

    Privacy

    The response confirms your own store. It never discloses the identities of any other account that may have stored the same record. A non-exclusive store is logged for every account that makes one; no account is shown another’s.

    Map it to your system

    Keep the record_id (and the stored_timestamp) alongside the matching submission in your own database, so each lead in your CRM is permanently linked to the record that proves it.

    ← PreviousInspect APINext →Integrate API
    Read me

    One API key works for Inspect, Store and Integrate. Like every Attestio API, deliveries never contain a lead’s personal data: they carry the record ID and its proof, which your system links to the lead it already holds.

    1Overview & the event model

    Inspect and Store are calls your system makes. Integrate works the other way round: you tell Attestio once where to send events, and Attestio pushes each one to your system as it happens. No polling, no batch exports.

    Its guiding principle is the proof travels with the lead. When a record is stored, your CRM receives the record ID, the store receipt and the consent evidence, ready to attach to the lead it already has.

    No personal data

    Payloads never include names, emails, phone numbers or addresses. Your CRM already holds those from the form; the record_id is what links the two.

    2Base URL & authentication

    All requests use HTTPS and are versioned in the path, on the same host as Inspect and Store.

    Base URL
    https://api.attestio.com/v1

    Every request authenticates with an API key from Settings → API keys, sent as a bearer token. A missing or invalid key returns 401.

    Header
    Authorization: Bearer atk_live_7f3aQm2Xk9…

    3Create an integration

    POST/v1/integrations

    Request body

    FieldTypeDescription
    eventstringRequired
    The event to receive: record.stored, record.inspected or record.created.
    target_urlstringRequired
    The HTTPS address in your system that receives deliveries.
    includestring[]Optional
    Sections to add to each delivery: store, consent, form. record_id is always sent.
    descriptionstringOptional
    A label for your team, such as the system it feeds.
    201 Created · application/json
    {
      "integration_id": "Hc2Lp4QwZ8mT1xVb6RkN3g",
      "event": "record.stored",
      "target_url": "https://crm.example.com/hooks/attestio",
      "status": "active",
      "signing_secret": "whsec_…"
    }
    Signing secret

    The signing_secret is shown once, when the integration is created. Keep it with your receiver: it is how you confirm a delivery really came from Attestio (see §5).

    4Events & delivery payload

    EventSent when
    record.createdA form on one of your domains issues a new record.
    record.inspectedYour account, or an account a record is shared with, inspects it.
    record.storedYour account stores a record — through the API or the dashboard.

    Each delivery is a POST to your target_url, with a JSON body like this:

    Delivery · record.stored
    {
      "event": "record.stored",
      "delivery_id": "Qm9Xa2Kp1Lw4Tz8RbN0cVg",
      "record_id": "fyqJkSSJRNub0RxVxe8Wdw",
      "store": { "store_id": "7PF4ibByTPi-SLJHhPRHaA", "stored_timestamp": "2026-10-13T14:12:09.481Z" },
      "consent": { "text": "By clicking Submit, I agree to be contacted…" },
      "form": { "id": "nXj8yUJWTzoqLcir", "name": "Default form" }
    }

    5Checking delivery signatures

    Every delivery carries an X-Attestio-Signature header: an HMAC-SHA256 of the raw request body, keyed with your signing_secret, plus a timestamp. Compute the same HMAC on your side and accept the delivery only if they match and the timestamp is recent.

    Header
    X-Attestio-Signature: t=1760364729,v1=5b2e9c…
    • Respond quickly. Return any 2xx within 10 seconds; do slow work afterwards.
    • Retries. If your system doesn’t answer with 2xx, Attestio retries with growing gaps for up to 24 hours.
    • Idempotent. A retry repeats the same delivery_id. Store it and ignore duplicates.

    6Testing an integration

    POST/v1/integrations/{integration_id}/test

    Sends one sample delivery for the integration’s event to its target_url, marked "test": true, and returns the response your system gave. In the dashboard, the Integrate card’s Send test call does the same.

    Test deliveries

    A test delivery uses a sample record ID and never counts toward billing or your reports.

    ← PreviousStore APINext →Form Field Annotation Guide
    In short

    Add data-attestio-field="email" (or phone, first-name, …) to every field that holds contact details, and data-attestio-mask to anything that must never be seen. That is the whole job. Untagged fields keep working exactly as before.

    1Why tag your fields

    Tagging tells Attestio exactly which field is which, so advertisers can confirm each lead through the Inspect API. The two attributes do two different jobs:

    AttributeJobWhat happens to the value
    data-attestio-field="…"Mapping. Names the field (this input is the email).Captured as typed, under its standard name.
    data-attestio-maskMasking. Hides the field.Hidden in the visitor’s browser, kept out of the session replay and the form data. Only a one-way fingerprint (SHA-256) is kept.

    Use both on one field to name it and hide it. Inspect can still confirm a hidden field by comparing fingerprints.

    If you skip tagging

    The field is still captured and still shows in the session replay. Attestio guesses what it is from its name, id, type and autocomplete: names like email, phone, full_name, first_name, last_name, address, city, state, zip and dob are recognised, and so are type="email", type="tel" and standard autocomplete values. A field with a name like field_7 is captured but not mapped, so tag it to tell Attestio what it is.

    Never captured. Passwords, file uploads and buttons are left out of the form data, and so are hidden technical fields such as CSRF tokens and TrustedForm or Jornaya certificate IDs. Empty fields are skipped.

    2How the tracker works

    What the Attestio snippet does with your form, step by step.

    1. Every field is captured, tagged or not. Each field is saved under its own name (or id), which is what the certificate shows. Passwords, file uploads, buttons, hidden technical fields and empty fields are left out.

    2. It reads the tags live from the page, in this order.

    • data-attestio-field="email" (or phone, first-name, …) tells Attestio what the field is.
    • data-attestio-mask hides the field. It is kept out of the session replay and the stored form data.
    • The older attestio-* classes still work as a fallback, also on a form builder’s wrapper around a single field.
    • data-attestio-unmask keeps a field visible and beats every masking rule.

    3. No tag: Attestio guesses. It reads the field’s name, id, type and autocomplete. Names like email, phone, first_name, address and state are recognised. A name like field_7 stays unmapped but is still captured.

    4. Some fields are hidden automatically. Date of birth, SSN and card fields are hidden by their name or autocomplete, even without a tag, and so is any type turned on under Sensitive fields in the dashboard.

    5. A hidden field keeps a fingerprint when its type is known. The type comes from the tag, from Sensitive fields, or from the guess in step 3. The fingerprint (SHA-256) is made in the visitor’s browser before the value is dropped, so the value itself never leaves the page. A hidden field whose type is unknown is hidden with no fingerprint.

    6. The tags are sent with the submit. Only the tag names travel, never values. Attestio uses them as the field’s official meaning, so Inspect knows which field is the email, phone or name, whatever the field is called.

    7. The record ID is added to the form. A hidden attestio_record_id field carries it to your server, and window.Attestio.getRecordID() returns it. A multi-step form keeps one record ID, and the tags, fingerprints and form data of every step are combined into that one record.

    When an advertiser checks a lead

    Inspect compares the advertiser’s fingerprint of an email, phone or name with what Attestio captured: first the fingerprint of a hidden field, then the field mapped by its tag or guess, then any field whose name looks right. A match is reported as true; Attestio never returns the value itself.

    3Tag the contact fields

    Add data-attestio-field to the field element itself — the input, select or textarea. Values are lowercase. Your existing classes and attributes stay as they are.

    ValueUse it onSame as autocomplete
    nameA single full-name fieldname
    first-nameFirst namegiven-name
    last-nameLast namefamily-name
    emailEmail addressemail
    phonePhone; add it to every part of a split phone fieldtel
    address-line1Street addressaddress-line1
    address-line2Apartment, suite or unitaddress-line2
    cityCityaddress-level2
    stateState or regionaddress-level1
    postal-codeZIP or postal codepostal-code
    consentThe consent checkbox—
    tcpaThe element that holds your TCPA disclaimer text, so the record keeps the exact wording—
    HTML · a typical lead form
    <form action="/submit" method="post">
      <input type="text"  name="fname"  data-attestio-field="first-name">
      <input type="text"  name="lname"  data-attestio-field="last-name">
      <input type="email" name="em"     data-attestio-field="email">
      <input type="tel"   name="ph"     data-attestio-field="phone">
    
      <input type="text"  name="street" data-attestio-field="address-line1">
      <input type="text"  name="apt"    data-attestio-field="address-line2">
      <input type="text"  name="city"   data-attestio-field="city">
      <select name="st"                data-attestio-field="state">…</select>
      <input type="text"  name="zip"    data-attestio-field="postal-code">
    
      <button type="submit">Get my quote</button>
    </form>

    4Hide sensitive fields

    Add data-attestio-mask to any field that should never be seen: a Social Security number, card or bank details, date of birth, income. The value is hidden in the visitor’s browser and never reaches the session replay or the form data.

    HTML · hidden fields
    <!-- Hidden. Shows as •••• in the replay and is left out of the form data -->
    <input type="text" name="ssn_last4" data-attestio-mask>
    <input type="text" name="acct_no"   data-attestio-mask>
    <input type="date" name="birthday"  data-attestio-mask>

    Name it and hide it. Put both attributes on one field to hide a contact detail and still let advertisers confirm it. Attestio keeps only a fingerprint, under that field’s name.

    HTML · mapped and hidden
    <!-- Hidden; its fingerprint is kept under "email" for Inspect -->
    <input type="email" name="em" data-attestio-field="email" data-attestio-mask>
    Don’t use the mask on its own for contact fields

    data-attestio-mask alone hides the value, and Attestio then has to guess what the field was from its name, id, type or autocomplete (for example name="email" or type="tel"). When it can, it keeps a fingerprint under that type and Inspect can still match it; a name is matched only when the full name, or both first and last name, are hidden. When the field’s name says nothing, such as field_7, no fingerprint is kept and Inspect can only match it if its type is set under Sensitive fields (below). For an email, phone or name you want hidden, always add both attributes.

    Matching a masked SSN, date of birth, card or income. Set the field’s type under Forms and Domains → Sensitive fields in the dashboard. Attestio then fingerprints it under that type (ssn, dob, card, income) for the Inspect API. SSN, date of birth and card are switched on by default, so those are fingerprinted without any change; you only need this for income, or if one of the defaults was switched off.

    Masked automatically. These fields are hidden even without the attribute:

    • autocomplete="bday" and any autocomplete="cc-…" (card) field
    • fields whose name or id contains ssn, social, dob, birth, cardnumber or creditcard

    Opting out. If one of those is not sensitive on your form, add data-attestio-unmask to it. It beats every other masking rule.

    HTML · opting out
    <!-- "social_handle" matches the built-in rule but is not sensitive here -->
    <input type="text" name="social_handle" data-attestio-unmask>

    5Special cases

    Phone split into three boxes. Put the same tag on every box. Attestio joins them, in page order, into one phone number.

    HTML · split phone
    <input type="tel" name="phone_area"   maxlength="3" data-attestio-field="phone">
    <input type="tel" name="phone_prefix" maxlength="3" data-attestio-field="phone">
    <input type="tel" name="phone_line"   maxlength="4" data-attestio-field="phone">

    First and last name. Tag each with its own value. When they are masked, the two are joined into one name fingerprint, so an advertiser matches the full name.

    HTML · first and last name
    <input name="fname" data-attestio-field="first-name" data-attestio-mask>
    <input name="lname" data-attestio-field="last-name"  data-attestio-mask>

    Consent checkbox and TCPA text. Tag the checkbox the person ticks, and the element that holds the disclaimer, so the record keeps the exact wording they saw.

    HTML · consent
    <label>
      <input type="checkbox" name="agree" data-attestio-field="consent">
      I agree to be contacted.
    </label>
    
    <p data-attestio-field="tcpa">
      By clicking "Get my quote", I consent to receive calls and texts from …
    </p>

    Multi-step forms. Tag the fields on every step. Attestio combines the tags from all steps into one record, so the final record knows every field.

    6Your record ID

    Every visit Attestio records gets a record ID, a 22-character code such as kq2b7w3m9paXc4TzR1vY0Q. Keep it with the lead in your own system and pass it on to your buyers: it is the ID they use with the Inspect and Store APIs.

    In a hidden field, automatically. Attestio adds a hidden field named attestio_record_id to your form. When the form is submitted, the record ID reaches your server or CRM with the rest of the lead. You don’t add this field yourself.

    What your form sends
    <!-- Added by Attestio; you don't write this -->
    <input type="hidden" name="attestio_record_id" value="kq2b7w3m9paXc4TzR1vY0Q">

    In JavaScript, for more flexibility. If your form is sent by your own code, or you want the record ID somewhere else, read it with window.Attestio.getRecordID().

    JavaScript · a form sent with fetch
    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      const lead = Object.fromEntries(new FormData(form));
      lead.attestio_record_id = window.Attestio?.getRecordID();
      await fetch('/api/leads', { method: 'POST', body: JSON.stringify(lead) });
    });
    Good to know

    window.Attestio exists once the Attestio snippet has loaded, so call getRecordID() when the form is submitted, not as the page opens. On a multi-step form, the same record ID covers every step. The hidden field is added to every form on the page: if your server rejects fields it does not expect, allow attestio_record_id; if your code picks which fields to send, add it as in the example above.

    7Rules

    RuleWhat it means
    One value per fielddata-attestio-field holds a single value. Give each field its own; if two separate fields carry the same value, the one submitted last wins. The only exception is a split phone, where every box shares phone.
    What names a fielddata-attestio-field first, then an attestio-* class (on the field, then on a single-field wrapper), then a guess from the field’s name, id, type or autocomplete.
    What hides a fielddata-attestio-unmask beats everything. Then data-attestio-mask or the attestio-mask class, then your dashboard settings and the automatic rules.
    Masking only addsA tag can hide more than your dashboard settings, never less.
    Unknown valuesA value not in the table, such as data-attestio-field="emial", is ignored: the field is captured as if untagged. The Field reader in onboarding flags it.
    Names, never valuesAttestio reads which tag sits on which field. The tags themselves carry no personal data.
    ← PreviousIntegrate API
    Attestio

    Consent documentation and lead intelligence for regulated lead generation. Prove every lead. Trust every decision.

    Product
    Overview Consent capture Records Lead scoring Double opt-in Session replay Pricing
    Compare
    All comparisons vs TrustedForm vs Verfi vs Jornaya vs ExpressConsent TrustedForm alternatives Migrate to Attestio
    Learn
    TCPA guide What is the TCPA? Compliance checklist State mini-TCPA laws Penalty calculator Consent hub FCRA CIPA FDCPA
    Company
    Industries Developers API referenceForm Field Annotation Guide Support About Status
    © 2026 Attestio. All rights reserved.Proudly made in the USA Privacy policy · Terms of service · System status

    Attestio is a software company, not a law firm, and does not provide legal advice or determine whether consent is legally sufficient. Attestio records document specified technical events and data; admissibility, evidentiary weight and legal effect depend on the circumstances.