# Signing Packets

> Let one recipient review and sign up to 50 documents in one Korala session

Source: https://docs.korala.ai/guides/signing-packets

---

A signing packet gives one recipient a single Korala review session and one
confirmation for a fixed set of documents. Use packets when your application
prepares documents for a manager, officer, or investor who must decide whether
to sign them.

Packet management belongs to the authenticated public API. Your backend uses
an API key to create and send the packet. Email verification is the default.
You may opt into a bearer link when your delivery channel provides the required
assurance.

  Use [bulk signing](./bulk-signing) when your trusted backend has authority to
  countersign documents with a saved signature. Use a signing packet when the
  recipient must review the documents and authorize the signatures.

## Limits and requirements

A packet must meet these requirements:

- Include 1 to 50 distinct documents with no more than 1 GiB of source files.
- Use documents from the API key's organization and from one mode: live or
  sandbox.
- Assign one signer from each document. Every assigned signer must have the
  packet recipient's email address.
- Put each document in `pending` status before you send the packet.
- Use a signer who has not signed or declined that document.

Version one supports one recipient and one item per document. A packet can
contain documents where the same recipient acts for different entities or in
different capacities.

In the dashboard, packets appear as **signing requests** alongside individual
document requests under **Documents → Signing requests**. Start from documents
or a collection, select prepared documents, and choose **Send for signing**.
Collections organize the source documents and templates; a request records a
particular set prepared for a recipient. The API continues to call grouped
requests signing packets.

## Create a packet

Create the documents, signers, and fields through the normal document APIs.
Send each document before you send the packet, then pass its document and
signer IDs to `signingPackets.create`.

```typescript

const korala = new KoralaClient({
  apiKeyId: process.env.KORALA_API_KEY_ID!,
  apiSecret: process.env.KORALA_API_SECRET!,
});

const packet = await korala.signingPackets.create({
  name: "Series A closing",
  externalId: "closing-2026-0142",
  accessMode: "email_verification",
  recipient: {
    name: "Alex Manager",
    email: "alex@example.com",
    externalId: "fund-manager-17",
  },
  items: [
    {
      documentId: subscription.id,
      signerId: subscriptionSigner.id,
      order: 1,
      representedEntity: "Example Fund I, L.P.",
      signingCapacity: "General Partner",
    },
    {
      documentId: sideLetter.id,
      signerId: sideLetterSigner.id,
      order: 2,
      representedEntity: "Example Fund I, L.P.",
      signingCapacity: "General Partner",
    },
  ],
  redirectUrl: "https://fund.example.com/closings/2026-0142",
  expiresAt: "2026-09-30T18:00:00.000Z",
});
```

`externalId` acts as an organization-scoped reconciliation key. If a retry
uses the same value, Korala returns the existing packet. Use a stable value
from your system for create retries.

Set `accessMode: 'link'` only when possession of the complete URL should grant
recipient access. Korala adds a separate 256-bit secret to `signingUrl`. The
packet public ID remains non-secret and cannot open the packet by itself.
Forwarding the complete link forwards signing authority. Link recipients must
adopt a fresh signature during the packet session; Korala neither exposes nor
accepts a saved signature through bearer-link access.

You may update `name`, `redirectUrl`, and `expiresAt` while the packet remains
in `draft`. Packet membership becomes fixed at creation and cannot change
after send.

## Lock values for the recipient's fields

Some values exist only after an earlier signer has finished. A borrower
signs a loan agreement, your underwriting decides the approved amount, and a
lending officer countersigns through a packet. The document is already
`pending`, so you cannot edit its fields, and you do not want the officer to
type the amount.

Pass `fieldValues` on a packet item to fix values for that item's signer.
When you send the packet, Korala writes the values, locks the fields, and
shows them to the recipient as read-only. The recipient still reviews each
document, accepts the electronic-signing disclosure, and authorizes the
signatures. Korala never signs for them.

```typescript
const packet = await korala.signingPackets.create({
  name: "Loan countersignature",
  externalId: "loan-2026-0917-officer",
  recipient: { name: "Sam Officer", email: "sam@example.com" },
  items: [
    {
      documentId: loanAgreement.id,
      signerId: officerSigner.id,
      representedEntity: "Example Bank N.A.",
      signingCapacity: "Lending Officer",
      fieldValues: [
        { fieldId: approvedAmountField.id, value: "18500.00" },
        { fieldId: officerNameField.id, value: "Sam Officer" },
        { fieldId: officerTitleField.id, value: "Lending Officer" },
      ],
    },
  ],
});
```

Each entry names a field by ID. Korala checks these rules at create time and
answers `400` without creating a packet when one fails:

- The field belongs to the item's signer on the item's document. Korala
  refuses a field of any other signer, including one who has signed.
- The field is a text, date, dropdown, or checkbox field. The recipient
  adopts signatures and initials in the ceremony.
- The field is neither shared-fill nor part of a choose-one group.
- Nobody has filled the field yet. Korala keeps a value the signer entered
  and a non-editable template prefill; you may lock an editable prefill.
- The value passes the field's own rules: dropdown options, `maxLength`,
  `inputMask`, and `validationPattern`. A failing value returns the field's
  `validationMessage`. You can lock a checkbox only as checked.
- Each field appears once per item, and the value is not blank.

A draft packet changes nothing on the document. When you send the packet,
Korala applies the values inside the transaction that freezes the manifest:
each field receives the value, a filled timestamp, and `editable: false`, and
Korala computes the item's signing-revision hash from the locked fields. If a
locked field no longer belongs to the packet signer, send answers `409` and
changes nothing. Other signers, their fields, and their signatures stay as
they are, so a borrower who signed first does not sign again.

The hosted page and the direct signing link both refuse to change a locked
field, and both show it as a muted value with a "set by the sender" hint,
distinct from the green style of the recipient's own entries. The management
API returns each item's `fieldValues`, the hosted page returns the fields with
their locked values and `filledBy: "sender"`, and the signed PDF and the
Certificate of Completion carry the values the same way as signer-entered
ones. Each locked field also writes a `field_locked` audit event on the
document naming the packet, item, field, organization actor, sealed revision,
and packet manifest hash.

To change a locked value before you send, void the draft and create a new
packet with a new `externalId`. A create that repeats an `externalId` returns
the existing packet unchanged. After send, the values belong to what the
recipient authorized and cannot change.

The document's Certificate of Completion lists each lock as
`Field Locked by the sender for <recipient email>`. Lock events share the
certificate's per-field cap with `field_filled` events, so they cannot push
consent and signature evidence off the page.

## Send the packet

Korala can email the recipient or return a URL for your application to use.

```typescript
const sent = await korala.signingPackets.send(packet.id, {
  delivery: "email", // use 'none' when your application handles the redirect
  electronicDisclosure: {
    id: "acme-electronic-signing-terms",
    version: "2026-09-01",
    title: "Electronic signature disclosure",
    contentHtml: "<p>Contact Acme Legal for a paper copy.</p>",
  },
});

console.log(sent.signingUrl);
console.log(sent.deliveryStatus); // 'sent', 'failed', or 'not_requested'
```

Sending freezes a copy of each source PDF and records its hash in the packet
manifest. Later document changes cannot alter what the recipient authorized.
An optional `electronicDisclosure` replaces the default modal content for this
packet. Korala sanitizes and freezes it at send time, then binds its profile
ID, version, and content hash to consent evidence and the packet Certificate of
Completion.

The response returns the committed packet even when email delivery fails.
Check `deliveryStatus`; call `remind(packet.id)` to send another invitation.
With `delivery: 'none'`, direct the recipient to `signingUrl`. The default
`email_verification` mode asks them to verify the packet email before showing
any document. The opt-in `link` mode opens the packet from the complete URL.
Korala requires electronic-signing consent in both modes and records the same
action evidence.

## Recipient experience

The Korala page uses the same PDF signing viewer as a single-document request.
Fields appear at their PDF coordinates, with the same input controls,
validation, signature capture, zoom, text layer, and keyboard behavior. The
packet sidebar adds the ordered document list and shows the represented entity
and signing capacity for the open document.

The recipient may leave an item unselected. Korala enables the final action
after the recipient opens every selected document and completes every required
field. The button states how many documents it will sign. Korala captures
initials separately when an initials field is present, then applies the
signature and initials to their matching fields in every selected document.

Korala records consent and the final confirmation before it queues the work.
The page then displays progress while the worker signs each selected document.
An invalid document does not roll back documents that the worker signed before
it encountered the problem.

## Track completion

List packets for a dashboard or reconciliation job:

```typescript
const page = await korala.signingPackets.list({
  status: "sent",
  search: "Series A",
  page: 1,
  limit: 20,
});
```

The document picker endpoint returns only pending, unexpired documents with a
signer who can still act. Filter it by recipient after the first selection:

```typescript
const eligible = await korala.signingPackets.listEligibleDocuments({
  recipientEmail: "alex@example.com",
  sandbox: false,
  limit: 50,
});
```

Read the packet when you need its current aggregate and item states:

```typescript
const current = await korala.signingPackets.get(packet.id);

console.log(current.status);
for (const item of current.items) {
  console.log(item.documentId, item.status);
}
```

Packet statuses are `draft`, `sent`, `viewed`, `processing`,
`partially_completed`, `recipient_completed`, `voided`, and `expired`. Item
statuses are `pending`, `signed`, and `invalidated`.

Use webhooks instead of frequent polling. Packet events cover packet progress,
while the existing document events continue for each document. See the
[Webhooks guide](./webhooks#signing-packet-events).

## Certificates and audit records

Each signed document keeps its own audit trail, signed PDF, and certificate of
completion. The document certificate names the packet, item, action, and
manifest hash that authorized the signature.

The packet audit endpoint returns lifecycle events and a certificate URL after
the worker generates the packet Certificate of Completion:

```typescript
const audit = await korala.signingPackets.audit(packet.id);

for (const event of audit.events) {
  console.log(event.createdAt, event.eventType, event.actorType);
}

if (audit.certificateUrl) {
  console.log(`Packet Certificate of Completion: ${audit.certificateUrl}`);
}
```

The packet certificate records the frozen manifest, document hashes, recipient
consent, confirmation text, actor evidence, and result for each selected item.
See the [Audit Trail guide](./audit-trail#signing-packet-evidence).

Korala digitally signs the completed document, its document Certificate of
Completion, and the packet Certificate of Completion. Live-mode artifacts also
receive an RFC 3161 timestamp. Sandbox artifacts use the platform's sandbox
certificate and skip the external timestamp authority.

## Partial completion, expiry, and voiding

The worker validates document state, signer order, required fields, expiration,
and the frozen revision before it signs an item. The packet enters
`partially_completed` when some selected items cannot proceed. The recipient
can return and submit the remaining eligible items.

Packet expiry blocks further recipient actions. An underlying document can
also expire before the packet. Voiding a packet blocks new packet actions but
does not undo signatures that Korala has already applied to its documents.

```typescript
await korala.signingPackets.void(packet.id);
```

## Signature evidence

Korala associates the recipient's email verification, consent, confirmation,
IP address, user agent, manifest hash, and item results with the signing
action. Each document retains separate evidence so a verifier can inspect one
signed PDF without the rest of the packet.

Electronic-signature requirements depend on the document and governing law.
Some forms require a separate attestation or a higher signature level. Korala's
platform certificate seals completed PDFs; it is not a signer-specific
qualified electronic signature under eIDAS.

## Organize with collections

Use `collectionId` when listing eligible documents and `sourceCollectionId`
when creating a packet from a reviewed collection selection. Filter packet
history by `sourceCollectionId`. Later collection edits do not alter the
packet manifest, and archiving does not interrupt existing signing.

See [Collections](https://docs.korala.ai/guides/collections) for scope, archive behavior, request
limits, and API and SDK examples.
