Skip to Content
SDKsReact SDK

React SDK

The @korala/react package provides React components and hooks for embedding document signing in your application.

Installation

npm install @korala/react # or yarn add @korala/react # or pnpm add @korala/react

Quick Start

import { KoralaSigner } from '@korala/react'; function SigningPage({ token }: { token: string }) { return ( <KoralaSigner token={token} signingUrl="https://app.korala.ai" style={{ width: '100%', height: '800px' }} onSigned={(data) => { console.log('Document signed!', data.documentId); }} onError={(data) => { console.error('Signing error:', data.message); }} /> ); }

Components

KoralaSigner

Renders an embedded signing iframe. The signer views the document and completes all assigned fields without leaving your app, from typed text fields to drawn signatures. The signer fills in any field you create without a value, so your app doesn’t have to collect that data first. See Collecting Information from Signers.

<KoralaSigner token={signingToken} signingUrl="https://app.korala.ai" className="signing-frame" style={{ width: '100%', height: '100%' }} allowedOrigins={['https://app.korala.ai']} onReady={(data) => console.log('Ready:', data.version)} onLoaded={(data) => console.log('Document loaded:', data.documentName)} onViewed={(data) => console.log('Viewed at:', data.viewedAt)} onFieldFilled={(data) => console.log(`${data.filledFields}/${data.totalRequiredFields} fields`)} onSigned={(data) => console.log('Signed:', data.documentId)} onDeclined={(data) => console.log('Declined:', data.reason)} onError={(data) => console.error(data.message)} />

Props

PropTypeRequiredDescription
tokenstringYesSigner access token (from the API)
signingUrlstringNoBase URL of the signing app. Defaults to https://korala.ai
classNamestringNoCSS class for the iframe
styleCSSPropertiesNoInline styles for the iframe
allowedOriginsstring[]NoRestrict which origins can send events. Accepts all if omitted
themeKoralaSignerThemeNoCustomize accent color, radius, font, background, and (on white-label plans) a logo
hideHeaderbooleanNoHide the signer header (document name + “Signing as”)
completionMessagestringNoCustom message on the completion screen (max 200 chars)
showNavigationbooleanNoShow Korala’s page/field navigation inside the embed (off by default)
hideAttributionbooleanNoRemove the “Powered by Korala” footer. Requires a white-label plan
onReady(data) => voidNoIframe initialized
onLoaded(data) => voidNoDocument loaded in viewer
ownConsentbooleanNoDeprecated compatibility option that suppresses Korala’s disclosure and consent capture. See below
onViewed(data) => voidNoSigner viewed the document
onConsentAccepted(data) => voidNoSigner agreed to sign and receive the document electronically
onFieldFilled(data) => voidNoA field was filled
onSigned(data) => voidNoSigner completed signing
onDeclined(data) => voidNoSigner declined to sign
onError(data) => voidNoAn error occurred

Electronic records disclosure

By default the signer sees a line beside the Complete button saying that completing means agreeing to sign and receive the document electronically, with a link to the full disclosure. Completing records a consent_accepted event in the document’s audit trail with the signer’s IP address, user agent and time, and fires onConsentAccepted.

To use your own terms, configure the disclosure profile when your backend sends the document. Korala keeps the final-action affirmation, renders your content in its disclosure modal, and records the profile version and content hash in the audit trail.

ownConsent remains available for integrations that already present and record consent outside Korala:

<KoralaSigner token={token} ownConsent />

If you must keep this legacy mode, record the agreement from your backend so it still reaches the audit trail:

api_request "POST" "/api/v1/signing/$SIGNER_TOKEN/consent"

Call it before the signer completes. Korala does not record agreement when ownConsent is set unless your backend makes this call. A bodyless call records external consent with the signer identity and time, without claiming which disclosure the partner showed. Keep your own copy of those terms and the acceptance evidence. New integrations should use a disclosure profile instead.

Custom clients that display the snapshot returned by GET /signing/:token can send { "disclosureSha256": "<electronicDisclosure.contentSha256>" } to the consent endpoint. Korala checks the hash before recording the snapshot identity. A mismatch returns 409; reload the disclosure and ask the signer to agree again.

Theme Customization

Match the signing UI to your brand by passing a theme prop. The theme replaces only accent-colored elements (buttons, field borders, focus rings); semantic colors (green success, red error, amber waiting) stay the same.

import { KoralaSigner } from '@korala/react'; import type { KoralaSignerTheme } from '@korala/react'; const theme: KoralaSignerTheme = { accentColor: '#2563EB', // hex color borderRadius: 8, // 0-24 px fontFamily: 'Inter, system-ui, sans-serif', // CSS font-family backgroundColor: '#EEF2FF', // page background (hex) logoUrl: 'https://cdn.acme.com/logo.png', // header logo (white-label plans) }; <KoralaSigner token={signingToken} theme={theme} style={{ width: '100%', height: '100%' }} onSigned={(data) => console.log('Signed:', data.documentId)} />

Theme Properties

PropertyTypeDefaultDescription
accentColorstring#D4503CHex color for buttons, borders, and focus states
borderRadiusnumber0Border radius in pixels (clamped 0-24) for buttons, modals, and inputs
fontFamilystringinheritCSS font-family applied to the signing UI
backgroundColorstringplatform defaultHex color for the page background
logoUrlstringnoneHTTPS URL of a header logo. Requires a white-label plan

All properties are optional. Without a theme, the signing UI uses the default Korala styling.

White-label (paid plans). logoUrl and hideAttribution apply only when the document’s organization is on a white-label plan. On other plans Korala ignores them and keeps the “Powered by Korala” footer. Accent, radius, font, and background work on every plan. Korala resolves the plan server-side, so passing these props from a non-entitled org changes nothing.

Default theme (coral accent, zero radius):

Default signing theme

Custom blue theme (accentColor: '#2563EB', borderRadius: 8):

Blue custom theme

KoralaSignaturePad

A canvas-based signature drawing pad. Useful for capturing signatures before uploading them via the API.

import { KoralaSignaturePad } from '@korala/react'; function CaptureSignature() { const [signature, setSignature] = useState<string | null>(null); return ( <KoralaSignaturePad width={600} height={200} penColor="#000000" onSignatureCreated={({ imageDataUrl }) => { setSignature(imageDataUrl); }} onClear={() => setSignature(null)} /> ); }

Props

PropTypeDefaultDescription
widthnumber600Canvas width in pixels
heightnumber200Canvas height in pixels
penColorstring#000000Stroke color
classNamestringNoneCSS class for the container
styleCSSPropertiesNoneInline styles
onSignatureCreated(data) => voidNoneCalled with { imageDataUrl } (base64 PNG) when a stroke ends
onClear() => voidNoneCalled when the canvas is cleared

Hooks

useKoralaEvents

Track the signing status and event history from any component. Useful when you need signing state outside the KoralaSigner component.

import { useKoralaEvents } from '@korala/react'; function SigningStatus() { const { status, events, lastEvent } = useKoralaEvents({ signingUrl: 'https://app.korala.ai', }); return ( <div> <p>Status: {status}</p> <p>Events received: {events.length}</p> {status === 'signed' && <p>Document signed.</p>} </div> ); }

Status Values

StatusDescription
loadingInitial state, waiting for iframe
readyIframe initialized
loadedDocument loaded in viewer
viewedSigner has viewed the document
signingThe signer is filling a field
signedSigning completed
declinedSigner declined
errorAn error occurred

useKoralaSignerRef

Control the signing iframe from your own UI.

import { KoralaSigner, useKoralaSignerRef } from '@korala/react'; function ControlledSigner({ token }: { token: string }) { const { ref, close, getStatus, gotoField, gotoPage, getFields } = useKoralaSignerRef(); return ( <div> <KoralaSigner ref={ref} token={token} /> <button onClick={close}>Close signing</button> <button onClick={getStatus}>Check status</button> <button onClick={() => gotoField()}>Go to next field</button> </div> ); }
MethodReturnsWhat it does
close()NoneCloses the signing session
getStatus()KoralaGetStatusResultDocument status and progress counts
gotoField(fieldId?)KoralaGotoFieldResultScrolls to a field; omit the id for the next unfilled one
gotoPage(pageNumber)KoralaGotoPageResultScrolls to a 1-based page
getFields()KoralaGetFieldsResultEvery field: type, label, page, required, filled

Navigation methods move the signer and nothing else. They will not fill a field, tick a checkbox, or open the signature pad. Your app cannot know what the signer can see, so it takes them to the field and leaves the acting to them.

Korala’s own navigation controls

Embeds hide the signing viewer’s page and previous/next-field controls by default. Set showNavigation to render them instead of building controls in your host app:

<KoralaSigner ref={ref} token={token} showNavigation />

Direct signing links show the controls.

Checking what the viewer supports

The SDK and the signing app ship separately, so your app can be on a newer SDK than the page it embeds. The ready event lists the commands the viewer understands; older signing apps omit it and support only close and get_status. Calling an unsupported command leaves its promise pending until the SDK’s timeout, so feature-detect rather than assume:

const [commands, setCommands] = useState<string[]>([]); <KoralaSigner ref={ref} token={token} onReady={(data) => setCommands(data.commands ?? [])} />; {commands.includes('goto_field') && ( <button onClick={() => gotoField()}>Go to next field</button> )}

Building your own field navigation

Long agreements usually put the signature block in the last third of the document. getFields and gotoField together let you put next/previous controls, a field list, or a progress rail in your own chrome.

function FieldNavigator({ token }: { token: string }) { const { ref, gotoField, getFields } = useKoralaSignerRef(); const [fields, setFields] = useState<KoralaFieldSummary[]>([]); // The list is available once the document has loaded. const load = async () => setFields((await getFields())?.fields ?? []); return ( <div> <KoralaSigner ref={ref} token={token} onLoaded={load} onFieldFilled={load} /> <ol> {fields.map((field) => ( <li key={field.fieldId}> <button onClick={() => gotoField(field.fieldId)}> {field.label ?? field.fieldType}: page {field.pageNumber} {field.filled ? ' ✓' : ''} </button> </li> ))} </ol> </div> ); }

gotoField resolves once the viewer has stopped scrolling:

interface KoralaGotoFieldResult { fieldId: string | null; pageNumber: number | null; outcome: 'arrived' | 'unreachable' | 'not_found'; }

arrived means the field is on screen. not_found means no field matched the id. unreachable means the viewer could not bring it into view. Surface that outcome instead of treating it as success.

interface KoralaFieldSummary { fieldId: string; fieldType: string; label: string | null; pageNumber: number; // 1-based required: boolean; filled: boolean; shared: boolean; // another signer's field that you may fill } interface KoralaGetFieldsResult { fields: KoralaFieldSummary[]; nextFieldId: string | null; // what the signer's own button targets next }

Events

The signing iframe sends events to the parent window via postMessage, each prefixed with korala:.

Event Payloads

Ready

interface ReadyEventData { version: string; // Embed protocol version token: string; // Signer access token }
interface ConsentAcceptedEventData { documentId: string; signerId: string; consentedAt: string; // ISO timestamp }

Does not fire when ownConsent is set.

Document Loaded

interface LoadedEventData { documentId: string; documentName: string; signerName: string; signerEmail: string; totalFields: number; requiredFields: number; }

Document Viewed

interface ViewedEventData { documentId: string; signerId: string; viewedAt: string; // ISO 8601 }

Field Filled

interface FieldFilledEventData { fieldId: string; fieldType: string; // 'signature' | 'initials' | 'date' | 'text' | 'checkbox' pageNumber?: number; // 1-based page; absent on signing apps older than the SDK filledFields: number; totalRequiredFields: number; // live; see below }

totalRequiredFields is a live denominator, not a constant. Choose-one groups count as one unit, and a conditionally required field joins or leaves the count as the signer toggles its controller. Checking a W-9’s LLC box raises it by one, unchecking lowers it. Render progress from the latest event rather than caching the number from document.loaded.

Document Signed

interface SignedEventData { documentId: string; signerId: string; signedAt: string; // ISO 8601 redirectUrl?: string; }

Document Declined

interface DeclinedEventData { documentId: string; signerId: string; reason?: string; declinedAt: string; // ISO 8601 }

Error

interface ErrorEventData { code: string; message: string; recoverable: boolean; }

Type Guard

Use isKoralaEvent to filter Korala events from other postMessage traffic:

import { isKoralaEvent } from '@korala/react'; window.addEventListener('message', (event) => { if (isKoralaEvent(event)) { console.log('Korala event:', event.data.type); } });

Full Example

For a complete working app with single-signer, multi-signer, and batch countersign demos, see the example repository on GitHub .

A minimal signing flow: create a document via the API, then embed the signing experience.

import { useState } from 'react'; import { KoralaSigner } from '@korala/react'; export function SigningFlow() { const [status, setStatus] = useState<'form' | 'signing' | 'complete'>('form'); const [token, setToken] = useState<string | null>(null); async function startSigning(name: string, email: string) { // Call your backend to create a document and get a signing token const res = await fetch('/api/create-signing-session', { method: 'POST', body: JSON.stringify({ name, email }), }); const { signingToken } = await res.json(); setToken(signingToken); setStatus('signing'); } if (status === 'signing' && token) { return ( <div style={{ height: '100vh' }}> <KoralaSigner token={token} signingUrl={process.env.NEXT_PUBLIC_KORALA_URL} style={{ width: '100%', height: '100%', border: 'none' }} onSigned={() => setStatus('complete')} onDeclined={(data) => { alert(`Signing declined: ${data.reason}`); setStatus('form'); }} onError={(data) => { console.error('Error:', data.message); if (!data.recoverable) setStatus('form'); }} /> </div> ); } if (status === 'complete') { return <p>Document signed.</p>; } return ( <form onSubmit={(e) => { e.preventDefault(); const fd = new FormData(e.currentTarget); startSigning(fd.get('name') as string, fd.get('email') as string); }}> <input name="name" placeholder="Name" required /> <input name="email" type="email" placeholder="Email" required /> <button type="submit">Start signing</button> </form> ); }
Last updated on