# Sealing

Prepare canonical PDFs for signing

Canonical URL: https://docs.paradoc.dev/guides/sealing-and-conversion/

Rendering produces a document in its native layer format. **Sealing** prepares a
filled form for signing by producing one canonical, flattened PDF, its SHA-256
hash, and the signature-field map. Conversion may happen anywhere; placement
and hashing always run locally, so both are verifiable.

## Declare signature slots

A layer declares its signing fields once, in a `signatures` map. Each slot
binds a party to a placement:

```ts
.inlineLayer('agreement', {
  mimeType: 'text/markdown',
  text: '... Sign: {{signature(parties.client, "client-sig")}} ...',
  signatures: {
    'client-sig': {
      party: { role: 'client' },
      type: 'signature',
      placement: 'flow',                                   // sits where the text puts it
    },
    'witness-sig': {
      party: { role: 'witness' },
      type: 'signature',
      placement: {
        anchor: { text: 'Witnessed by:', offsetY: 12 },     // text-located
        width: 200,
        height: 40,
      },
    },
    'client-date': {
      party: { role: 'client' },
      type: 'date_signed',
      placement: { page: 2, x: 400, y: 700, width: 100, height: 20 },  // fixed
    },
  },
})
```

Three placement strategies, one engine:

| Placement | How the position is found                                                                                                          | Use when                                    |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `'flow'`  | An invisible marker is injected at the slot's placeholder during rendering and located in the converted PDF                        | Flowing text templates (markdown, HTML)     |
| `anchor`  | Literal document text is found in the converted PDF; `occurrence` picks a match when the text repeats, otherwise it must be unique | Text known at design time, position unknown |
| absolute  | Declared page/x/y coordinates                                                                                                      | PDF templates, fixed layouts                |

Misconfiguration fails **before** any rendering or conversion with a
`SealConfigError` naming every problem: unknown party roles, a
required-signature party no slot places, or a required slot without a
signatory.

`validate()` and `paradoc validate` report the problems the definition alone
shows, before any data exists: a slot for an undeclared role, a
required-signature party with no slot on a layer that declares `signatures`,
a `'flow'` slot on a PDF or DOCX layer or of a type other than `signature` or
`initials`, and a `'flow'` slot that no `signature(...)` or `initials(...)`
directive in the layer's template places. See
[`paradoc validate`](/cli/commands/validate#layer-references).

## Seal a PDF layer locally

PDF layers need no adapter or network call. Paradoc renders the layer,
resolves placements, flattens, and hashes the exact canonical bytes. The
resolver that reads the PDF template's bytes is bound where the form is
constructed — `p.form(schema, { resolver })` — so `draft` already carries
it:

```ts
const signable = await draft.seal()

signable.canonicalPdfBytes // Uint8Array
signable.canonicalPdfHash  // sha256:...
signable.signatureMap      // provider-neutral PDF coordinates
```

## Seal markdown, HTML, or DOCX

Non-PDF layers need a **converter**: a `SealAdapter` that turns the rendered
document into PDF bytes and nothing else. Placement is not the converter's
job — core locates anchors and markers itself with the built-in locator, so a
pure byte converter works with zero configuration:

```ts
import { hostedSealAdapter } from '@paradoc/sdk'

const signable = await draft.seal({
  adapter: hostedSealAdapter({ apiKey }),
})
```

A custom converter is one honest function:

```ts
import type { SealAdapter } from '@paradoc/sdk'

const adapter: SealAdapter = {
  async convert({ document }) {
    return { pdf: await myHtmlToPdf(document.content) }
  },
}
```

If an adapter does resolve placements itself (returning a complete
`signatureMap`), core respects it and skips the locator. Pass `locate` to
substitute a custom locator tier.

For `'flow'` slots, core renders twice: a marker pass locates every
placeholder in the converted PDF, then a clean render becomes the canonical
document. If the marker's width shifts a line wrap between passes, sealing
fails loud rather than misplacing a field.

## Inspect before sealing

`prepareSeal()` resolves the signature map without flattening, hashing, or
leaving the draft phase — useful for envelope flows that need coordinates
before committing:

```ts
const prep = await draft.prepareSeal({ adapter })

prep.pdf           // the converted PDF the coordinates describe (pre-flatten)
prep.signatureMap  // resolved fields
prep.provenance    // per field: 'declared' | 'anchor' | 'marker'
prep.warnings      // e.g. slots skipped for unfilled party indexes

const signable = await draft.seal({ adapter })  // same map, canonicalized
```

## Older artifacts

Layers in older schema versions could declare slots in `signatureBlocks` and
`anchorBlocks`. Those fields are removed (see the
[version history](/schemas#version-history)). Run
`paradoc migrate` to move each block into `signatures` under the same id.

## Which operation to use

| Goal                                | Operation                    |
| ----------------------------------- | ---------------------------- |
| Fill a layer in its native format   | `createLayerRenderer()`      |
| Resolve placements without sealing  | `.prepareSeal({ adapter? })` |
| Prepare a PDF layer for signing     | `.seal()`                    |
| Prepare a non-PDF layer for signing | `.seal({ adapter })`         |
