---
title: "Signatures — React"
description: "Sign a signature field with a mark — drawn, typed, uploaded — through a signer you bring; fill it visually without sealing; validate what is signed; drop an armed mark onto a field."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/plugins/signature"
---

# Signatures

A signature field is signed with two things: a **mark** (what the reader sees
in the field) and a **key** (what seals the bytes). The signature plugin owns
the act and nothing else. Marks are stamp-library assets — a person's
signature and initials are one library of kind `signatures`, drawn, typed or
uploaded through the stamp plugin — and the key is a *signer port* you
configure. The mark's page is drawn into the field by the engine; the plugin
composes nothing.

Register `signaturePlugin()` beside `formPlugin()` and `stampPlugin()`, read
the capability with `useSignature()`:

**`basic.tsx`**

```tsx
import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, useDocumentId, useKernelValue } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import { formPlugin, formWidgetRenderer, useForm, useFormSnapshot } from '@embedpdf/react/form';
import { stampPlugin, useStamp, useStampAssetPreviewUrl } from '@embedpdf/react/stamp';
import {
  createTestSigner,
  signaturePlugin,
  useSignature,
  useSignatureSnapshot,
  useSignatureTarget,
  useSignerRows,
} from '@embedpdf/react/signature';
import type { SignerRow } from '@embedpdf/react/signature';
import { localEngine } from '@embedpdf/engine';

import {
  Button,
  Demo,
  Readout,
  Spacer,
  StageFrame,
  Toolbar,
  stageFill,
} from '../stage/_shared/chrome';

const engine = localEngine();
// [!signer]
// A throwaway key for the demo. Bring your own with `webCryptoSigner`, a
// service with `remoteSigner`, or a persisted personal one with `personalSigner`.
const signer = createTestSigner({ commonName: 'Demo signer' });
// [!/signer]
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  formPlugin(),
  stampPlugin({ assetEngine: engine }),
  signaturePlugin({
    signer: () => signer,
    // Trust the demo key itself, so its signatures validate as 'valid'.
    trust: { anchors: async () => [(await signer).certificate] },
  }),
];
const renderers = [formWidgetRenderer];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

/** The ebook has no signature field: author one on the first page, once. */
function useSignatureField() {
  const form = useForm();
  const fields = useFormSnapshot()?.fields ?? null;
  const documentId = useDocumentId();
  const firstPage = useKernelValue((k) =>
    documentId
      ? (k.getState().core.documents[documentId]?.pages[0]?.pageObjectNumber ?? null)
      : null,
  );
  useEffect(() => {
    if (!fields || firstPage === null || fields.some((f) => f.family === 'signature')) return;
    form
      .placeField({
        family: 'signature',
        pageObjectNumber: firstPage,
        box: { x: 60, y: 620, width: 220, height: 64 },
      })
      .catch((err) => console.error(err));
    // Once the snapshot is known; the field appearing is the outcome.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [fields === null, firstPage]);
}

/** One person, made once: a library of kind 'signatures' with a typed mark. */
function usePerson(): SignerRow | null {
  const stamp = useStamp();
  const rows = useSignerRows();
  useEffect(() => {
    if (rows.length > 0) return;
    stamp
      .createLibrary('Ada Lovelace', { kind: 'signatures' })
      .then((libraryId) =>
        stamp.addAsset({
          libraryId,
          name: 'signature',
          label: 'Signature',
          mark: { kind: 'text', text: 'Ada Lovelace', fontFamily: 'times-italic', fontSize: 36 },
        }),
      )
      .catch((err) => console.error(err));
    // Create once per workspace; the row list changing is the outcome.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [stamp]);
  return rows[0] ?? null;
}

function MarkButton({
  assetId,
  label,
  onPick,
}: {
  assetId: string;
  label: string;
  onPick: () => void;
}) {
  const url = useStampAssetPreviewUrl(assetId);
  return (
    <Button title={label} onClick={onPick}>
      {url ? <img src={url} alt={label} style={{ height: 22 }} /> : label}
    </Button>
  );
}

function SignBar() {
  useSignatureField();
  const person = usePerson();
  const signature = useSignature();
  const snapshot = useSignatureSnapshot();
  const { target, busy } = useSignatureTarget();
  const [error, setError] = useState<string | null>(null);
  const field = snapshot?.signatures[0] ?? null;

  const pick = (assetId: string) => {
    setError(null);
    const destination = target ?? field?.field;
    if (!destination) return;
    // The destination decides: a signature field → sign it (mode 'sign').
    signature
      .placeMark({ assetId }, { field: destination })
      .catch((err) => setError(err instanceof Error ? err.message : String(err)));
  };

  if (!person) return <Readout>Creating a signature…</Readout>;
  const verdict = field && signature.verdictOf(field.field);
  return (
    <Toolbar>
      <Readout>{person.name}</Readout>
      {person.signatures.map((asset) => (
        <MarkButton
          key={asset.id}
          assetId={asset.id}
          label={asset.label}
          onPick={() => pick(asset.id)}
        />
      ))}
      <Spacer />
      <Readout>
        {error
          ? `Error: ${error}`
          : busy
            ? 'Signing…'
            : !field
              ? 'Adding a signature field…'
              : field.signed
                ? `Signed by ${field.signer.name ?? '?'} — ${verdict?.summary ?? 'validating…'}`
                : target
                  ? 'Field selected: pick the mark'
                  : 'Click the field, or pick the mark'}
      </Readout>
    </Toolbar>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <Demo>
        <DocumentGate fallback={<p>Loading…</p>}>
          <SignBar />
          <StageFrame height={420}>
            <Stage style={stageFill}>
              {() => (
                <>
                  <RenderLayer annotations={false} />
                  {/* the signature widget renders "sign here" (sets the target) or "inspect" */}
                  <AnnotationLayer renderers={renderers} />
                </>
              )}
            </Stage>
          </StageFrame>
        </DocumentGate>
      </Demo>
    </Viewer>
  );
}
```

```ts
import { signaturePlugin, personalSigner, indexedDbKeyStore } from '@embedpdf/react/signature';

signaturePlugin({
  // The key holder. A thunk resolves per signing (prompt a PIN, fetch a token…).
  signer: () =>
    personalSigner({ subject: 'Ada Lovelace', store: indexedDbKeyStore('my-app-keys') }),
  // Anchors for validation; without them every verdict tops out at "valid, signer unknown".
  trust: { anchors: async () => [rootCertificateDer] },
  // 'sign' (default with a signer) | 'visual' (default without) | 'ask'
  mode: 'sign',
});
```

## The destination decides

One call places a mark; where it lands says what happens:

```ts
const signature = useSignature();

// A signature field: sign it (mode 'sign'), draw the mark in without sealing
// (mode 'visual'), or emit `ask` for your own dialog (mode 'ask').
await signature.placeMark({ assetId }, { field: { kind: 'fqn', name: 'sig' } });

// Anywhere else: a stamp, exactly as the stamp plugin would place it.
await signature.placeMark({ assetId }, { pageObjectNumber, at: { x: 120, y: 90 } });
```

The pointer follows the same rule. With the interaction hub and the stamp
plugin present, a mark armed from a `signatures` library (`stamp.armAsset`)
and clicked over an **unsigned** signature field goes into the field; over a
signed field the click is consumed and nothing happens; everywhere else the
annotation plugin drops it as a stamp. Preview's behaviour, with cryptography
behind it.

A mark is a stamp-plugin asset or bytes you bring:

```ts
type Mark = { assetId: string } | { source: BinarySource }; // PNG, JPEG, or a one-page PDF
```

## Sign, fill, clear

```ts
// Seal: the mark becomes the widget's appearance, the signer signs the digest.
const result = await signature.signField({
  field: { kind: 'fqn', name: 'sig' },
  mark: { assetId },
  attribution: { reason: 'Approved', location: 'Amsterdam' }, // /Name defaults to the certificate's subject
  certify: { permission: 2 }, // optional: the certification signature (needs doc.sign.certify)
});
result.version; // the new saved version the sealed bytes became

// Visual only: the mark is drawn into an unsigned field; nothing is sealed.
await signature.fillField(field, { source: pngBytes });
await signature.clearField(field);
```

`signField` runs the engine's two-phase signing (`prepare` → your signer →
`complete`) through `@embedpdf/core-signature`; any failure aborts the
candidate, so a document is never left with a pending signing. A signed
field refuses both `fillField` and `clearField`: its appearance is part of
what was signed.

> Signing needs `doc.sign` (a certification also `doc.sign.certify`); a visual fill
> needs `doc.forms.fill`. `canSign()`, `canFill()` and `canCertify()` mirror those for
> your buttons.

## Signer ports

The engine never holds a key. A port is either a **raw signer** (you hold the
key; the CMS is built here) or a **CMS signer** (a service builds it; only the
digest leaves):

```ts
import {
  webCryptoSigner,
  remoteSigner,
  personalSigner,
  createTestSigner,
} from '@embedpdf/react/signature';

// Bring your own key and certificate chain.
webCryptoSigner({ privateKey, certificateChain: [leafDer, issuerDer] });

// Your signing service or HSM turns the digest into a detached CMS.
remoteSigner({
  sign: ({ digest, algorithm, subFilter }) => api.sign(digest, algorithm, subFilter),
});

// One self-signed identity per person, the private key non-extractable in IndexedDB.
await personalSigner({ subject: 'Ada Lovelace', store: indexedDbKeyStore('embedpdf-signers') });

// Throwaway, for tests and demos.
await createTestSigner({ commonName: 'Demo' });
```

A personal signer is Preview's experience with real cryptography behind it:
a reader who trusts the certificate validates the signature; every other
reader sees "validity unknown", the honest answer for a self-issued identity.

## Reading and validating

```ts
signature.snapshot(); // revisions, every signature field, the protection in force
signature.signatureOf({ kind: 'fqn', name: 'sig' }); // one field's facts (or by { annotObjectNumber })
signature.protection(); // what the document's signatures forbid from now on

const verdicts = await signature.validate(); // integrity · cryptography · trust · changes since
signature.verdictOf(field)?.summary; // 'valid' | 'valid-untrusted' | 'invalid' | 'indeterminate'

const analysis = await signature.analyze({ since: { signatureIndex: 0 } }); // what changed, rule by rule
const bytes = await signature.revisionBytes(dto.revisionIndex); // the exact bytes the signature covers
```

The plugin judges the **working copy** by default: unsaved edits count, so the verdict is the one
the file a save produces will get. `validate({ until: 'persisted' })` judges the loaded bytes only;
`verdict.modifications.basis` says which bytes a verdict is about. After every annotation or form
edit the plugin re-judges on its own, and the moment an unsaved edit turns a signature that held into
one a save would invalidate, it emits `invalidating` once — the warning Acrobat shows.

```ts
signature.onChanged((e) => {
  if (e.type === 'invalidating') toast(`This change will invalidate ${e.field} when saved`);
});
```

What invalidates what is the engine's judgement (see the engine's signatures page): after a plain
approval signature, form fill-in and further signatures keep it valid, an annotation does not. A
signer who wants comments to stay valid certifies with `P=3` — with `allowCertify` on, the first
signature emits `ask` in mode `sign` too, so a chrome can offer that choice.

The plugin also re-reads and re-validates on `document.versioned` — a signature completed here or,
on the cloud, in another session.

## The target and the chrome's intents

"Select the field, then pick a mark" is one piece of state: the **target**.
A "sign here" control sets it; the next mark picked goes there. The plugin
also emits the intents a chrome turns into surfaces:

```ts
signature.setTarget(field); // the next mark goes here; null clears
signature.onChanged((e) => {
  // 'signed' | 'filled' | 'cleared' | 'validated' | 'protectionChanged'
  // 'target'  → open your signatures panel
  // 'ask'     → mode 'ask': open your sign dialog, then signField / fillField
  // 'inspect' → a signed field was activated: show its verdict
});
```

The React form view already wires the widgets: an unsigned signature field
renders a "sign here" control that sets the target; a signed one emits
`inspect`.

## A person is a library

The marks live in the stamp plugin, as one library per person of kind
`signatures` holding a `signature` asset and, optionally, `initials`:

```ts
const libraryId = await stamp.createLibrary('Ada Lovelace', { kind: 'signatures' });
await stamp.addAsset({
  libraryId,
  name: 'signature',
  label: 'Signature',
  mark: { kind: 'ink', strokes },
});
await stamp.addAsset({
  libraryId,
  name: 'initials',
  label: 'Initials',
  mark: { kind: 'text', text: 'AL', fontFamily: 'times-italic' },
});

useSignerRows(); // [{ libraryId, name, signatures, initials }] — derived, nothing stored beyond the file
```

Rename, export and delete are the library verbs; persistence is whatever
store you gave `persistStampLibraries`. The file is Acrobat-readable like any
stamp library.

## Permissions

Signing rides `doc.sign` and, for a certification, `doc.sign.certify`. A
visual fill is a form write (`doc.forms.fill`). Reading signatures rides
`doc.forms.read`; `revisionBytes` egresses content and is gated by
`doc.download`. On a signed document the engine additionally enforces what
its signatures forbid (`ProtectedDocument`); `protection()` tells you before
you try.
