Terms review, signature and status, rendered inside your own tools - your counterparty never leaves Salesforce, ServiceNow, Jira, or your internal dashboard. The element is called <exact-paper> for historical reasons; the name is fixed because it ships in the widget.
Get the embed widget running in 2 lines of code
<script src="https://exact.works/embed/paper.js"></script>
<exact-paper listing="contract-reviewer" theme="dark" /><exact-paper> web componentCustomize the widget with attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
listing* | string | - | Slug of the offering the widget should open |
theme | 'dark' | 'light' | 'dark' | Color theme for the widget |
return-url | string | - | URL to redirect to once the record is sealed |
api-key | string | - | API key for programmatic access (server-side usage) |
buyer-email | string | - | Pre-fill the buyer's email address |
min-height | string | '600px' | Minimum height of the iframe |
Listen for lifecycle events posted by the flow
The script validates the message origin and then re-dispatches any exact:paper:* message the embedded flow posts, on both the element and document. Event names are part of the shipped widget and are not being renamed.
| Event | Description | Payload |
|---|---|---|
exact:paper:compiled | The terms have been sealed. The hash is the one anyone can recheck at /verify/paper/<id>. | { paperId, hash, purchaseId } |
exact:paper:accepted | The counterparty accepted the terms as presented. | { paperId } |
exact:paper:settled | Legacy. Named for a settlement flow that has been withdrawn — exact.works holds no funds and settles nothing. Do not build on it. | { paperId } |
exact:paper:error | An error occurred during the process | { error } |
// Listen on the element
document.querySelector('exact-paper')
.addEventListener('exact:paper:compiled', (e) => {
console.log('Record ID:', e.detail.paperId)
console.log('Hash:', e.detail.hash)
// Update your CRM, create a case, etc.
updateSalesforceCase(e.detail.paperId)
})
// Or listen globally on document
document.addEventListener('exact:paper:accepted', (e) => {
// Store the id alongside your own record, close the ticket, etc.
attachRecordId(e.detail.paperId)
})Embed a status view for existing Papers
Use the status iframe directly to show progress on an existing record, without the purchase flow.
<iframe
src="https://exact.works/embed/paper/status?paperId=xxx&theme=dark"
style="width: 100%; min-height: 400px; border: none; border-radius: 12px;"
allow="payment"
title="Paper Status"
></iframe>| Parameter | Type | Description |
|---|---|---|
paperId | string | The Paper ID to display |
theme | 'dark' | 'light' | Color theme (default: 'dark') |
Embed in a Visualforce page or Lightning Web Component.
<apex:page>
<script src="https://exact.works/embed/paper.js"></script>
<exact-paper
listing="{!listing}"
theme="light"
buyer-email="{!Contact.Email}"
return-url="{!URLFOR($Action.Case.View, Case.Id)}"
/>
<script>
document.addEventListener('exact:paper:compiled', function(e) {
// Update the Case with Paper ID
sforce.one.navigateToSObject('{!Case.Id}', 'detail');
});
</script>
</apex:page>Add to a UI Page widget in the Service Portal.
<div id="exact-paper-container"></div>
<script>
// Load the embed script
var script = document.createElement('script');
script.src = 'https://exact.works/embed/paper.js';
script.onload = function() {
var paper = document.createElement('exact-paper');
paper.setAttribute('listing', 'contract-reviewer');
paper.setAttribute('theme', 'light');
paper.setAttribute('buyer-email', '{{data.user.email}}');
document.getElementById('exact-paper-container').appendChild(paper);
};
document.head.appendChild(script);
</script>Create a Forge custom panel with the embed iframe.
import React, { useEffect, useState } from 'react';
import ForgeUI, { Fragment, IssuePanel, render, useProductContext } from '@forge/ui';
const Panel = () => {
const context = useProductContext();
const issueKey = context.platformContext.issueKey;
return (
<Fragment>
<IssuePanel>
<Text>exact.works Integration</Text>
<CustomUIExtension
src={`https://exact.works/embed/paper?listing=code-reviewer&theme=light`}
/>
</IssuePanel>
</Fragment>
);
};
export const run = render(<Panel />);Use the web component in your React app.
'use client'
import { useEffect, useRef } from 'react'
declare global {
namespace JSX {
interface IntrinsicElements {
'exact-paper': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
listing: string
theme?: 'dark' | 'light'
'return-url'?: string
'api-key'?: string
'buyer-email'?: string
},
HTMLElement
>
}
}
}
export function ExactPaperEmbed({ listing, onCompiled }: {
listing: string
onCompiled?: (data: { paperId: string; hash: string }) => void
}) {
const ref = useRef<HTMLElement>(null)
useEffect(() => {
// Load script once
if (!document.querySelector('script[src*="exact.works/embed/paper.js"]')) {
const script = document.createElement('script')
script.src = 'https://exact.works/embed/paper.js'
document.head.appendChild(script)
}
// Listen for events
const handler = (e: CustomEvent) => {
onCompiled?.(e.detail)
}
ref.current?.addEventListener('exact:paper:compiled', handler as EventListener)
return () => ref.current?.removeEventListener('exact:paper:compiled', handler as EventListener)
}, [onCompiled])
return <exact-paper ref={ref} listing={listing} theme="dark" />
}Worth knowing before you plan an integration around it
The widget covers formation: presenting terms, agreeing them, and sealing the result. It ends there. exact.works is not a party to the agreement it helps you draft.
The api-key attribute is for server-side usage only. Never expose API keys in client-side code. Use session-based authentication instead.
The embed script validates that postMessage events originate from exact.works. This prevents malicious iframes from spoofing Paper events.
All payment processing happens within the iframe via Stripe Elements. Card data never touches your domain. The allow="payment" attribute enables the Payment Request API for wallet payments.