<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adeola Okunola</title>
    <description>The latest articles on DEV Community by Adeola Okunola (@adeolaokunola).</description>
    <link>https://dev.to/adeolaokunola</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3922333%2Fbe6a65c1-ecea-4dc8-915e-01245ae7ba86.jpeg</url>
      <title>DEV Community: Adeola Okunola</title>
      <link>https://dev.to/adeolaokunola</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adeolaokunola"/>
    <language>en</language>
    <item>
      <title>How to Build Audit Logs That Actually Prove Something</title>
      <dc:creator>Adeola Okunola</dc:creator>
      <pubDate>Mon, 06 Jul 2026 14:19:25 +0000</pubDate>
      <link>https://dev.to/adeolaokunola/how-to-build-audit-logs-that-actually-prove-something-2dop</link>
      <guid>https://dev.to/adeolaokunola/how-to-build-audit-logs-that-actually-prove-something-2dop</guid>
      <description>&lt;p&gt;Every app has an audit log. Almost none of them can actually prove anything.&lt;/p&gt;

&lt;p&gt;A typical audit log is a table in a database you control, with timestamps from a clock you can set. It's fine until someone has a reason to doubt it: an auditor, a customer, opposing counsel. At that point, "here's our log" means "trust our database," which is exactly what an audit is supposed to avoid.&lt;/p&gt;

&lt;p&gt;A trustworthy audit log needs three properties your database does not give you for free:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Integrity.&lt;/strong&gt; Each entry is provably unchanged since it was written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completeness.&lt;/strong&gt; A missing or reordered entry is detectable from the data itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portability.&lt;/strong&gt; Anyone can verify it without access to your systems and without trusting you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's how to get all three with the Invoance SDK, then stream the log to your SIEM and hand it to an auditor. Examples are in Node.js; the same API exists in Python, Go, Rust, Java, Ruby, .NET, and PHP.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Register the org you're logging for
&lt;/h2&gt;

&lt;p&gt;Audit events belong to an organization (your tenant, or an end customer). Register it once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InvoanceClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvoanceClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// reads INVOANCE_API_KEY&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;org_acme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme Inc.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Append a signed event
&lt;/h2&gt;

&lt;p&gt;Instead of an &lt;code&gt;INSERT&lt;/code&gt;, you append. Invoance canonicalizes the event, signs it with your tenant's own key, and writes it to an append-only ledger with a gapless sequence number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;org_acme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user.role.granted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;u_42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ada Lovelace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;u_99&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;203.0.113.7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event_id&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gapless sequence is what makes a &lt;strong&gt;missing&lt;/strong&gt; record detectable later. There's no delete and no update, so tampering means breaking either a signature or the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Verify it, without trusting the vendor
&lt;/h2&gt;

&lt;p&gt;This is the part your database can't do. The SDK ships the same verification the backend uses, so you can recompute an event's canonical hash and check its signature entirely client-side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verifyAuditEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyAuditEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// true if the signature and hash check out&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the recomputed hash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An auditor can run the exact same check against the tenant's published public key, with no account and no access to your systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Query and export for the auditor
&lt;/h2&gt;

&lt;p&gt;When someone asks for the trail, you hand over something that verifies itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;org_acme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user.role.granted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;rangeStart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Or queue a packaged export (CSV or NDJSON)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;org_acme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;csv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every record in that export carries its own signature, so the file is proof on its own, not a CSV anyone has to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Stream it to your SIEM
&lt;/h2&gt;

&lt;p&gt;Provable logs are more useful when they show up where your security team already works. Create a webhook stream and Invoance pushes every new signed event to it. The signing secret is returned once, so you can verify deliveries yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;org_acme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://siem.acme.com/hooks/invoance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// stream.signing_secret -&amp;gt; store it now; it's shown only once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Give your customers a hosted viewer
&lt;/h2&gt;

&lt;p&gt;When an end customer or their auditor wants to see the log directly, you don't have to build a UI. Mint a one-time, read-only hosted-viewer link scoped to their org:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;portal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;portalSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;org_acme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audit_logs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// portal.url -&amp;gt; a self-serve, verifiable view of that org's audit log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They browse and verify their own trail. You never expose your systems, and they never need an Invoance account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The audit-log API (plus events, documents, AI attestations, and traces) is available in official SDKs for 8 languages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📚 Docs and quickstarts: &lt;a href="https://www.invoance.com/developers" rel="noopener noreferrer"&gt;invoance.com/developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 Source: &lt;a href="https://github.com/Invoance" rel="noopener noreferrer"&gt;github.com/Invoance&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;invoance
pip &lt;span class="nb"&gt;install &lt;/span&gt;invoance
cargo add invoance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build the audit log you'd be comfortable handing to an auditor.&lt;/p&gt;

</description>
      <category>security</category>
      <category>compliance</category>
      <category>sdk</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How to Prove Your AI Did What It Said: A Developer's Guide to Verifiable AI Outputs</title>
      <dc:creator>Adeola Okunola</dc:creator>
      <pubDate>Mon, 22 Jun 2026 22:54:19 +0000</pubDate>
      <link>https://dev.to/adeolaokunola/how-to-prove-your-ai-did-what-it-said-a-developers-guide-to-verifiable-ai-outputs-3ik3</link>
      <guid>https://dev.to/adeolaokunola/how-to-prove-your-ai-did-what-it-said-a-developers-guide-to-verifiable-ai-outputs-3ik3</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.invoance.com/resources/how-to-prove-your-ai-output-developer-guide" rel="noopener noreferrer"&gt;the Invoance blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Your AI's output is gone the moment it returns
&lt;/h2&gt;

&lt;p&gt;Most teams treat AI outputs like ephemeral function returns. The model responds, the response goes to the user, maybe a row gets written to a database, and that is the entire trail. When a customer disputes an answer six months later, when a regulator asks what your model produced for user 4f9a on March 14 at 2:47 PM, or when your legal team needs to defend a decision in a deposition, that database row is your evidence.&lt;/p&gt;

&lt;p&gt;The problem is that database rows can be edited, deleted, or corrupted, accidentally or otherwise. There is nothing in the row that proves it has not been touched since the AI generated it. There is nothing that proves your timestamp column was not bulk-updated by a migration last quarter. There is nothing that proves the input column matches the prompt the model actually saw.&lt;/p&gt;

&lt;p&gt;Your logs are a story you are asking someone to take on faith. That works until somebody has a reason to challenge them. The first thing opposing counsel, an auditor, or a regulator will do is question the chain of custody. Without a cryptographic anchor, you have no answer to that question.&lt;/p&gt;

&lt;p&gt;AI attestation closes that gap. At generation time, you submit a small payload describing the call. The attestation service hashes the input, hashes the output, signs the whole thing with your tenant's private Ed25519 key, and writes it to an append-only ledger. You get back an attestation ID and a public verification URL. From that point on, anyone can verify what happened, without trusting you, your database, or even Invoance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What attestation actually proves (and what it does not)
&lt;/h2&gt;

&lt;p&gt;Precision matters here, because half the confusion in this category comes from people overclaiming what cryptographic proof gives you.&lt;/p&gt;

&lt;p&gt;Attestation proves five specific things. First, the exact output the model produced at the moment of attestation. Second, the exact input the model received. Third, which model and version produced it. Fourth, that the record has not been altered since it was signed. Fifth, that the record was issued by your organization, identified by your tenant's public key.&lt;/p&gt;

&lt;p&gt;Attestation does not prove that the output was correct, that the model's reasoning was sound, that the input was truthful, or that any human acted on the output appropriately. It does not establish legal admissibility on its own, although it produces exactly the kind of self-authenticating record that Federal Rules of Evidence 902(14) is designed for.&lt;/p&gt;

&lt;p&gt;The distinction matters because attestation is a technical guarantee, not an editorial one. What it gives you is the evidentiary foundation everything else rests on. Without it, every downstream argument about your AI's behavior starts from "trust our logs." With it, every downstream argument starts from a signed receipt that any third party can verify in under a second.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Attestation proves what your model said. It does not prove the model was right. Those are different problems, and conflating them is how organizations end up overpromising on governance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Your first attestation in three lines
&lt;/h2&gt;

&lt;p&gt;The Invoance Node SDK ships with a single ingest method. You instantiate the client (which reads INVOANCE_API_KEY from the environment), pass the input, output, and model metadata, and you receive an attestation record with an ID, a payload hash, and a created-at timestamp.&lt;/p&gt;

&lt;p&gt;This call sits alongside your existing inference pipeline. It does not modify your prompts, your model, or your response shape. It does not introduce latency on your AI response, the attestation runs after generation, in parallel with your own logging. It is one network call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InvoanceClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvoanceClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Reads INVOANCE_API_KEY=invoance_live_... from env&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Run your model however you do today&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Summarize this contract clause&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;yourLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userPrompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Attest the input/output pair&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;att&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attestations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;modelOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;modelProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;modelName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;modelVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2025-01-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;u_42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sess_4f9a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attestation_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → "att_01HXY..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you get back
&lt;/h2&gt;

&lt;p&gt;The response includes the attestation ID, the SHA-256 hashes of the input, output, and combined payload, the timestamp the record was sealed, and a status field that lets the SDK distinguish a fresh write from an idempotent retry. Every field on the response is significant.&lt;/p&gt;

&lt;p&gt;The attestation_id is the handle you store alongside your own database row, so the next time anyone asks about that specific decision, you can hand them the attestation. The input_hash and output_hash are what a verifier compares against when they want to confirm the content has not changed since it was signed. The payload_hash is the canonical hash that was actually signed, and it is what the public verification endpoint validates against your tenant's published public key.&lt;/p&gt;

&lt;p&gt;The status field is worth highlighting for anyone running at-least-once delivery patterns: if you submit the same payload twice, the second response returns status: "duplicate" instead of "accepted", and your client gets back the original attestation. The system will not silently double-write. This means you can wire the call into a retry loop without worrying about ledger pollution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attestation_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"att_01HXY7K3M9P2QV5R8WNBC4DAE6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-06T14:47:13.482Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"input_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"2c624232cdd221771294dfbb310aca000a0df6ac8b66b696d90ef06fdefb7a72"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accepted"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How a third party verifies it independently
&lt;/h2&gt;

&lt;p&gt;This is the part that separates real attestation from "we have logs and we promise nobody changed them." When you give an auditor, a customer, opposing counsel, or a regulator the attestation ID, they hit a public endpoint, no API key, no Invoance account, and receive the full proof bundle.&lt;/p&gt;

&lt;p&gt;The verification endpoint returns the original record (input hash, output hash, payload hash, signature, model metadata, timestamp), the public key that signed it, and a structured signature-verification result. They can also POST a content hash to the verify subroute and the service will tell them whether the content they hold matches what was sealed. If a customer disputes an output, you both compute the SHA-256 of the disputed text, send it to /verify, and the result is unambiguous.&lt;/p&gt;

&lt;p&gt;The signature is verifiable offline as well. Each tenant has its own Ed25519 keypair, and the tenant's public key is embedded inline in every proof bundle returned by the verification endpoint, so a verifier never needs a second round trip. If a verifier wants to pin the key independently, they can fetch it directly by the tenant's verified domain at GET /keys/{domain}, which returns the base64url-encoded public key, algorithm, and key ID. Either way, anyone with the record and the public key can verify the Ed25519 signature using any standard library, without contacting Invoance at all. That is the point. The proof does not depend on us being online, in business, or trusted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Anyone can hit this. No API key. No account.&lt;/span&gt;
curl https://api.invoance.com/v1/proof/ai/att_01HXY7K3M9P2QV5R8WNBC4DAE6

&lt;span class="c"&gt;# Or check whether a specific piece of content matches what was sealed:&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.invoance.com/v1/proof/ai/att_01HXY.../verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"content_hash": "2c624232cdd221771294dfbb310aca000a0df6ac8b66b696d90ef06fdefb7a72"}'&lt;/span&gt;

&lt;span class="c"&gt;# Response:&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "match": true,&lt;/span&gt;
&lt;span class="c"&gt;#   "signature_valid": true,&lt;/span&gt;
&lt;span class="c"&gt;#   "signed_by": "tenant_org_invoance",&lt;/span&gt;
&lt;span class="c"&gt;#   "signed_at": "2026-05-06T14:47:13.482Z"&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.invoance.com/developers/sdks?lang=python&amp;amp;section=attestations" rel="noopener noreferrer"&gt;AI attestation SDK reference&lt;/a&gt; — live, runnable code for creating and fetching AI attestations in Python and Node, with the request and response shapes used in this post.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://api.invoance.com/keys/invoance.com" rel="noopener noreferrer"&gt;Fetch a tenant's public key&lt;/a&gt; — each tenant has its own Ed25519 keypair. GET /keys/{domain} returns the base64url public key for offline verification.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Same flow, Python edition
&lt;/h2&gt;

&lt;p&gt;For ML and data teams who live in Python, the SDK exposes the same surface. The async client is the recommended path for production pipelines because attestation is an I/O-bound network call, blocking your inference loop on it would waste capacity. Use it inside an asyncio.gather() if you are attesting batches.&lt;/p&gt;

&lt;p&gt;The field names switch from camelCase to snake_case to match Python conventions, but the semantics are identical. The same ingest call, the same response shape, the same verification URL. If your stack is mixed (Python inference, Node application server), you can attest from one and verify from the other without coordination.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;invoance&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;InvoanceClient&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;attest_completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;InvoanceClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# reads INVOANCE_API_KEY
&lt;/span&gt;        &lt;span class="n"&gt;att&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attestations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;attestation_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model_provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2025-01-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sess_4f9a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attestation_id&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;attest_completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize this contract clause&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The clause states...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;u_42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The questions developers ask first
&lt;/h2&gt;

&lt;p&gt;On latency: the attestation call is independent of your inference path. Run it after you return the response to the user, or fire-and-forget into a queue. The end-user latency cost is zero if you do not block on it. The wall-clock cost of the call itself is typically under 100 ms.&lt;/p&gt;

&lt;p&gt;On PII: the attestation service stores hashes of the input and output by default, not the plaintext. If you want plaintext stored alongside the proof for replay (so auditors can read what the model said, not just verify the hash), opt in per attestation. If you do not, the only way the original content can be recovered is from your own systems, and the attestation only proves what hash was sealed.&lt;/p&gt;

&lt;p&gt;On batching: high-throughput pipelines attest hundreds of outputs per second by submitting in parallel. The ingest endpoint is idempotent on the payload-hash level, so duplicate submissions are deduplicated server-side and you can retry freely under at-least-once semantics.&lt;/p&gt;

&lt;p&gt;On cost: the Builder tier covers small dev workloads for free, the Growth tier covers most production AI startups, and Compliance and Enterprise tiers cover regulated workloads with retention guarantees, dedicated signing keys, and audit support. See the pricing page for current limits.&lt;/p&gt;

&lt;p&gt;On where the signature lives: each tenant has its own Ed25519 keypair. The signature is generated server-side using your tenant's private key, which is encrypted at rest with the platform master key and never leaves the backend. Your tenant's public key is embedded inline in every proof bundle returned by the verification endpoint, and is also fetchable independently by your verified domain at GET /keys/{domain}, so any third party can validate signatures with a standard Ed25519 library.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like to a compliance team
&lt;/h2&gt;

&lt;p&gt;If you are a developer and a colleague from compliance, GRC, or legal is reading this over your shoulder, here is the version they care about.&lt;/p&gt;

&lt;p&gt;Under the EU AI Act, high-risk AI systems are required to maintain logs of inputs and outputs sufficient for post-market monitoring and regulatory review. AI attestation produces those logs in a form that is independently verifiable, satisfying not only the existence requirement but the integrity requirement that traditional logging tends to fall short on.&lt;/p&gt;

&lt;p&gt;Under ISO 42001, the new AI management system standard, organizations must demonstrate accountability for AI system behavior through auditable records. Attestation provides the technical mechanism that auditors can sample directly, instead of relying on screenshots and database exports of unknown provenance.&lt;/p&gt;

&lt;p&gt;Under the NIST AI Risk Management Framework, the Measure and Manage functions both call for evidence-grade records of AI system behavior. Cryptographic attestation maps cleanly to those subcategories.&lt;/p&gt;

&lt;p&gt;Under Federal Rules of Evidence 902(14) in the United States, electronically stored records produced by a process that generates a unique identifier and authenticates the record are self-authenticating. Ed25519-signed attestations with publicly verifiable signatures are designed for exactly this provision.&lt;/p&gt;

&lt;p&gt;The point is that attestation is not a separate compliance project. It is one API call your engineering team makes, and the resulting evidentiary record is consumable by every downstream framework you are subject to.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Compliance teams: ask your engineers what percentage of high-stakes AI calls currently produce a signed, externally verifiable record. If the answer is zero, that is the gap. Attestation closes it without changing the AI pipeline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  From signup to your first attestation in five minutes
&lt;/h2&gt;

&lt;p&gt;The activation path is short by design. Sign up at the dashboard, create an organization, and your tenant's signing keys are generated automatically on first use. Generate an API key from the API Keys section. Install the SDK in your project (npm install invoance, or pip install invoance). Set INVOANCE_API_KEY in your environment. Make the call.&lt;/p&gt;

&lt;p&gt;The first attestation you make against your free tenant returns a real attestation ID, a real signature, and a real public verification URL. The same URL pattern that backs Compliance and Enterprise customers backs the Builder tier. The infrastructure is identical, the limits scale with the plan.&lt;/p&gt;

&lt;p&gt;If you are evaluating this for a regulated or high-volume use case, the public verification URL is the asset to share with internal auditors and external counsel before you commit. Hand them an attestation_id from a test run, let them verify it themselves, and the trust question collapses into a technical demo. That is usually enough to unblock procurement.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>cryptography</category>
      <category>compliance</category>
    </item>
  </channel>
</rss>
