Signatures
Walk a signer through anchored fields; host owns identity and crypto. Same flow via SDK callbacks or iframe envelopes.
Casual Editor ships a document-signature pipeline that drives signing flows — employment agreements, sales contracts, multi-party approvals. The editor handles the UX (walking the signer through anchored fields, capturing drawn / typed / uploaded signatures). The host owns identity, crypto, audit, and final stamping.
The same shapes work whether you deliver via the SDK or the iframe protocol — pick the delivery based on your host, not the feature.
What the editor handles
- Renders a floating right-anchored signing pane.
- Walks the signer through the configured fields (
sequentialorconcurrentmode). - Three capture surfaces: draw on a canvas (PNG), type a name (UTF-8 in a script font), upload an image (PNG/JPEG/SVG).
- Emits a progress event when each field is signed; a completion event when every required field is done.
- Honours cancellation from either side.
What the host owns
- The signer identity — the editor accepts whatever the host attests to via the auth token.
- The signature material — a PNG of a drawn signature, a typed string, an X.509 detached signature produced by a CA, whatever your flow needs. The editor receives bytes + mime and stamps them; it doesn’t verify anything.
- The audit trail — the protocol surfaces per-field events; you decide what to persist.
- Final stamping into the
.docx— for v1, the editor returns the unmodified document and afieldsmap of signed payloads. Your backend stamps the bytes (usingring/rustls/ OpenSSL — whatever you already trust).
This split keeps the editor cert-free (no X.509, no PKI lib in the bundle) and lets you plug in any signing backend — DocuSign, Adobe Sign, a custom HSM.
Field shape
interface SignatureField {
fieldId: string;
label: string; // 'Employee signature', 'Witness', etc.
required: boolean;
anchor:
| { kind: 'doc'; paraId: string; search?: string }
| { kind: 'sheet'; sheet: string; cell: string };
methods: Array<'drawn' | 'typed' | 'uploaded'>;
signer?: { name?: string; email?: string };
}
The anchor discriminator is the only product-specific piece. Casual Editor uses paragraph anchors; Casual Sheets uses cell anchors. Everything else — banner, mode, complete event, cancel — is uniform.
SDK integration
Pass a signing prop to <CasualEditor>. The wrapper renders the signing pane next to the editor and routes events to your callbacks.
<CasualEditor
fileSource={fs}
docId={docId}
signing={{
mode: 'sequential',
fields: [
{
fieldId: 'emp',
label: 'Employee',
required: true,
anchor: { kind: 'doc', paraId: 'A1B2C3' },
methods: ['drawn', 'typed'],
},
{
fieldId: 'mgr',
label: 'Manager',
required: true,
anchor: { kind: 'doc', paraId: 'D4E5F6' },
methods: ['drawn'],
},
],
banner: 'Signing as Alice for Acme Co.',
onFieldSigned: async ({ fieldId, method, bytes, mime, signedAt }) => {
await myBackend.audit.signatureField({ fieldId, method, signedAt });
},
onComplete: async ({ bytes, fields }) => {
const stamped = await myBackend.stampSignatures(bytes, fields);
await fs.save(docId, stamped);
onSigningDone();
},
onCancel: ({ reason }) => onSigningAborted(reason),
}}
/>
Standalone usage outside <CasualEditor> is also supported — <SigningProvider> + <SigningPane> + the three capture surfaces are exported individually for hosts that compose their own editor shell.
Iframe integration
For non-React hosts, the same shapes flow over postMessage. The host sends casual.signature.request to open a signing session; the editor responds with casual.signature.request.ack, then emits casual.signature.field.signed per field and casual.signature.complete at the end. Either side can send casual.signature.cancel.
// Host opens a signing session
iframe.contentWindow.postMessage(
{
type: 'casual.signature.request',
app: 'docs',
id: 'sig-1',
v: 1,
data: {
fields: [
/* ... */
],
mode: 'sequential',
banner: 'Signing as Alice for Acme Co.',
},
},
'https://editor.example.com',
);
// Listener receives per-field events
window.addEventListener('message', (e) => {
if (e.origin !== 'https://editor.example.com') return;
if (e.data?.type === 'casual.signature.field.signed') {
handleFieldSigned(e.data.data);
}
if (e.data?.type === 'casual.signature.complete') {
handleComplete(e.data.data);
}
});
Full envelope shapes in the iframe protocol contract.
Sequence — three-signer sequential flow
Host Editor
──── ──────
casual.signature.request(fields × 3, render dimmed chrome, show field 1
mode='sequential', banner) ───► banner: "Signing as Alice"
◄────── casual.signature.request.ack
…Alice draws her signature…
◄────── casual.signature.field.signed
host writes audit row show field 2
casual.command.setBanner ───────────────────► banner: "Signing as Bob"
…Bob types his name…
◄────── casual.signature.field.signed
…Carol uploads a PNG…
◄────── casual.signature.field.signed
◄────── casual.signature.complete
host stamps bytes, archives
Capability advertisement
The editor lists its signature support in the casual.hello handshake:
signature.drawnsignature.typedsignature.uploadedsignature.sequential/signature.concurrent
A host that asks for a method the editor doesn’t advertise gets signature.request.ack with ok: false, code: 'unsupported'. No surprises.
What’s deferred
- Editor-side stamping. v1 returns the unstamped bytes + signature payloads; your backend does the final composition. v2 lands editor-side stamping (image insertion at the anchor) so single-page hosts can finalize client-side.
- Cryptographic signatures. The protocol carries opaque bytes — for traditional drawn/typed/uploaded signatures, an image is fine. For PKI-grade signing (X.509 detached signatures, CAdES), the host produces the signature material from its CA and passes it via the
bytesfield; the editor just stamps it.
Sheet parity
Casual Sheets uses the same signing pipeline with cell anchors instead of paragraph anchors. See the Casual Sheets signatures guide.