# Bulk Signing

> Sign multiple documents at once using saved signatures

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

---

Bulk signing signs hundreds of documents in one API call. Use it when one signer (e.g., a CEO) needs to sign many documents of the same type.

## Overview

The bulk signing flow has three steps:

1. **Save a signature**: Capture and store the signer's signature image
2. **Create documents**: Create documents from a template with signers and fields
3. **Bulk sign**: Sign all documents at once using the saved signature

## Step 1: Save a Signature

Before bulk signing, you need a saved signature for the signer. You can capture signatures using the `<KoralaSignaturePad>` React component or any other method that produces a base64 PNG data URL.

> **Format requirement:** Signature images must be base64 PNG data URLs (`data:image/png;base64,...`). The API rejects JPEG, SVG, and regular HTTP URLs. The maximum size is 500KB.

### Using the React Signature Pad

```tsx

function SignatureCapture() {
  return (
    <KoralaSignaturePad
      onSignatureCreated={({ imageDataUrl }) => {
        // Send to your backend to save via Korala API
        fetch('/api/save-signature', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            signatureImageUrl: imageDataUrl,
            email: 'ceo@company.com',
            name: 'Jane Smith',
          }),
        });
      }}
    />
  );
}
```

### Saving via the API

    ```typescript
    import { KoralaClient } from '@korala/api-client';

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

    const signature = await korala.signatures.create({
      email: 'ceo@company.com',
      name: 'Jane Smith',
      signatureImageUrl: 'data:image/png;base64,...',
      isDefault: true,
    });

    console.log(`Saved signature: ${signature.id}`);
    ```
    ```bash
    curl -X POST https://api.korala.ai/api/v1/signatures \
      -H "Content-Type: application/json" \
      -H "X-API-Key: $API_KEY_ID" \
      -H "X-Timestamp: $TIMESTAMP" \
      -H "X-Signature: $SIGNATURE" \
      -d '{
        "email": "ceo@company.com",
        "name": "Jane Smith",
        "signatureImageUrl": "data:image/png;base64,...",
        "isDefault": true
      }'
    ```

### Managing Saved Signatures

```typescript
// List signatures for a specific email
const signatures = await korala.signatures.list({ email: 'ceo@company.com' });

// Get a specific signature
const signature = await korala.signatures.get(signatureId);

// Delete a signature
await korala.signatures.delete(signatureId);
```

## Step 2: Create Documents

Create your documents using templates. See the [Documents guide](./documents) for details on creating and sending documents.

```typescript
// Create 200 NDAs from a template
const documentIds: string[] = [];

for (const recipient of recipients) {
  const doc = await korala.templates.createDocument(templateId, {
    signerRoles: {
      'Company Representative': {
        email: 'ceo@company.com',
        name: 'Jane Smith',
      },
      'Recipient': {
        email: recipient.email,
        name: recipient.name,
      },
    },
  });

  documentIds.push(doc.id);

  // Send for signing
  await korala.documents.send(doc.id);
}
```

## Step 3: Bulk Sign

Sign all documents at once using the saved signature:

    ```typescript
    const result = await korala.documents.bulkSign({
      documentIds,
      signerEmail: 'ceo@company.com',
      autoFillDateValue: '2026-02-18', // optional, defaults to today
    });

    console.log(`Signed: ${result.signed}`);
    console.log(`Failed: ${result.failed}`);

    // Check individual results
    for (const doc of result.results) {
      if (doc.success) {
        console.log(`${doc.documentId}: signed (allSigned: ${doc.allSigned})`);
      } else {
        console.log(`${doc.documentId}: failed - ${doc.error}`);
      }
    }
    ```
    ```bash
    curl -X POST https://api.korala.ai/api/v1/documents/bulk-sign \
      -H "Content-Type: application/json" \
      -H "X-API-Key: $API_KEY_ID" \
      -H "X-Timestamp: $TIMESTAMP" \
      -H "X-Signature: $SIGNATURE" \
      -d '{
        "documentIds": ["doc-1", "doc-2", "doc-3"],
        "signerEmail": "ceo@company.com",
        "autoFillDateValue": "2026-02-18"
      }'
    ```

## How Auto-Fill Works

During bulk signing, Korala fills fields by type:

| Field Type | Auto-Fill Behavior |
|------------|-------------------|
| **Signature** | Uses the signer's default saved signature |
| **Initials** | Uses the signer's default saved signature |
| **Date** | Uses `autoFillDateValue` (defaults to today) |
| **Text** | Requires a prefilled value (e.g., from template variables) |
| **Checkbox** | Requires a prefilled value (e.g., from template variables) |

> **Important:** If no saved signature exists for the signer, any required Signature or Initials fields will fail with an error. You must save a signature via `POST /signatures` (Step 1) before calling bulk sign.

## Per-Document Overrides

You can override specific field values for individual documents:

```typescript
const result = await korala.documents.bulkSign({
  documentIds: ['doc-1', 'doc-2', 'doc-3'],
  signerEmail: 'ceo@company.com',
  documents: [
    {
      documentId: 'doc-3',
      fields: [
        // Override a text field value
        { fieldId: 'field-xyz', value: 'Special terms apply' },
        // Override a signature field with a different signature image
        {
          fieldId: 'field-sig',
          value: 'Signed',
          signatureImageUrl: 'data:image/png;base64,...',
        },
      ],
    },
  ],
});
```

Per-document overrides take precedence over auto-fill.

> **Note:** Signature images must be base64 PNG data URLs (`data:image/png;base64,...`). The API rejects other formats (JPEG, SVG, regular URLs).

## Handling Partial Success

Bulk sign always returns HTTP 200 with per-document results. Some documents may fail while others succeed:

```typescript
const result = await korala.documents.bulkSign({
  documentIds,
  signerEmail: 'ceo@company.com',
});

if (result.failed > 0) {
  const failures = result.results.filter((r) => !r.success);
  console.error('Failed documents:', failures);

  // Retry failed documents
  const retryIds = failures
    .filter((f) => f.error !== 'Signer has already signed')
    .map((f) => f.documentId);

  if (retryIds.length > 0) {
    await korala.documents.bulkSign({
      documentIds: retryIds,
      signerEmail: 'ceo@company.com',
    });
  }
}
```

## Identifying Signers

You can identify the target signer by email or external ID:

```typescript
// By email
await korala.documents.bulkSign({
  documentIds,
  signerEmail: 'ceo@company.com',
});

// By external ID
await korala.documents.bulkSign({
  documentIds,
  signerExternalId: 'user_123',
});
```

Provide exactly one of `signerEmail` or `signerExternalId`.

### When one person holds several roles

A packet can legitimately list the same person more than once. An officer who
signs for a company, for its parent guarantor and for a subsidiary appears as
three signers sharing an email at signing orders 1, 2 and 3.

`signerEmail` matches all three. Korala takes the **lowest signing order that
has not signed or declined**, so repeated calls walk the roles in order:

```typescript
// Call 1 signs signing order 1
await korala.documents.bulkSign({ documentIds, signerEmail: 'officer@example.com' });

// Call 2 signs signing order 2, and so on
await korala.documents.bulkSign({ documentIds, signerEmail: 'officer@example.com' });
```

Each call signs one signer per document, and the signing order rules below
still apply.

To name one role instead of taking whichever comes next, give each signer a
distinct `externalId` when you create the document, then address it with
`signerExternalId`:

```typescript
await korala.templates.createDocument(templateId, {
  name: 'Supply Agreement — Acme',
  signers: {
    Company: { name: 'J. Officer', email: 'officer@example.com', externalId: 'company' },
    Guarantor: { name: 'J. Officer', email: 'officer@example.com', externalId: 'guarantor' },
    Subsidiary: { name: 'J. Officer', email: 'officer@example.com', externalId: 'subsidiary' },
  },
});

await korala.documents.bulkSign({ documentIds, signerExternalId: 'guarantor' });
```

`externalId` belongs to one signer, so it still identifies a single role when
emails repeat, and it holds its meaning across every document you generate from
the template.

## IP Attribution

When your server calls the bulk sign API on behalf of end users, the audit trail records your server's IP address. To preserve the end-user IP for compliance, pass it via the `X-Client-IP` header. Korala stores the value as `claimedIpAddress` in the audit trail alongside the verified network IP.

    ```typescript
    const result = await korala.documents.bulkSign(
      {
        documentIds,
        signerEmail: 'ceo@company.com',
      },
      { clientIp: endUserIpAddress },
    );
    ```
    ```bash
    curl -X POST https://api.korala.ai/api/v1/documents/bulk-sign \
      -H "Content-Type: application/json" \
      -H "X-API-Key: $API_KEY_ID" \
      -H "X-Timestamp: $TIMESTAMP" \
      -H "X-Signature: $SIGNATURE" \
      -H "X-Client-IP: 203.0.113.42" \
      -d '{
        "documentIds": ["doc-1", "doc-2"],
        "signerEmail": "ceo@company.com"
      }'
    ```

> **Note:** The `X-Client-IP` value must be a valid IP address (IPv4 or IPv6). Korala ignores invalid values. Korala validates the header's format without verifying it; the recorded value is the partner's assertion of the end-user's IP.

## Limits

- Maximum **500 documents** per bulk sign request
- Documents must be in **Pending** status
- The target signer must not have already signed or declined
- Every signer at a lower signing order must have signed first, otherwise the
  document fails with `Waiting for prior signers: ...`

## Response Format

```json
{
  "signed": 198,
  "failed": 2,
  "results": [
    {
      "documentId": "doc-1",
      "success": true,
      "allSigned": true
    },
    {
      "documentId": "doc-2",
      "success": true,
      "allSigned": false
    },
    {
      "documentId": "doc-x",
      "success": false,
      "error": "Document not found"
    }
  ]
}
```

When `allSigned` is `true`, Korala has queued the document for completion (cryptographic signing and timestamping). Listen for the `document_completed` webhook to know when the signed PDF is ready.
