# Webhooks

> Receive real-time notifications for document events

Source: https://docs.korala.ai/guides/webhooks

---

Webhooks deliver real-time notifications to your application when document events occur.

## Event Types

| Event | Description |
|-------|-------------|
| `document_created` | Document was created |
| `document_sent` | Document was sent for signing |
| `document_viewed` | A signer opened the document |
| `document_signed` | A signer completed their signature |
| `document_completed` | All signers finished, document is sealed |
| `document_voided` | Document was cancelled |
| `document_declined` | A signer declined to sign |

### Signing packet events

Packet event names use dots to distinguish them from document events:

| Event | Description |
|-------|-------------|
| `signing_packet.sent` | Korala froze the manifest and sent the packet |
| `signing_packet.viewed` | The recipient opened the packet |
| `signing_packet.item_signed` | The worker signed one packet item |
| `signing_packet.recipient_completed` | The recipient signed all packet items |
| `signing_packet.partially_completed` | Some selected items could not complete |
| `signing_packet.voided` | The sender voided the packet |
| `signing_packet.expired` | The packet passed its expiration time |

Document events still fire for packet items. Subscribe to packet events for
the aggregate workflow and document events for signed-file readiness. A
`signing_packet.recipient_completed` event can arrive before
`document_completed` when another signer still has a turn on that document.

## Creating a Webhook

    ```typescript
    const webhook = await korala.webhooks.create({
      url: 'https://your-app.com/webhooks/korala',
      events: ['document_completed', 'document_signed'],
      mode: 'live', // or 'test' for sandbox documents
    });

    console.log(`Webhook ID: ${webhook.id}`);
    console.log(`Secret: ${webhook.secret}`); // Save this for verification!
    ```
    ```bash
    api_request "POST" "/api/v1/webhooks" \
      '{
        "url": "https://your-app.com/webhooks/korala",
        "events": ["document_completed", "document_signed"],
        "mode": "live"
      }'
    ```

Save the webhook `secret` when you create the webhook; Korala shows it once. You need it to verify webhook signatures.

## Test and Live Mode

Every webhook endpoint has a `mode`, and an endpoint only receives events that match it:

| Mode | Receives |
| --- | --- |
| `live` (default) | Events from real documents |
| `test` | Events from sandbox documents |

Sandbox activity never reaches a live endpoint, so you can develop against a test
endpoint without your production handler seeing the traffic or your
production events showing up in a local tunnel while you debug.

Register two endpoints to cover both:

```typescript
await korala.webhooks.create({
  url: 'https://your-app.com/webhooks/korala',
  events: ['document_completed', 'signing_packet.recipient_completed'],
  mode: 'live',
});

await korala.webhooks.create({
  url: 'https://staging.your-app.com/webhooks/korala',
  events: ['document_completed'],
  mode: 'test',
});
```

The payload's `data.sandbox` field still tells you which kind of document an
event came from, so a single handler can serve both endpoints if you prefer.

Session and workflow events have no sandbox equivalent and are always delivered
as live traffic.

## Webhook Payload

All webhooks follow this format:

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "event": "document_completed",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "documentId": "doc_xyz789",
    "name": "contract.pdf",
    "completedAt": "2024-01-15T10:30:00Z",
    "signerCount": 2,
    "sandbox": false
  }
}
```

### Event-Specific Data

Each event type has a typed data payload. Import the corresponding type from `@korala/api-client`:

```typescript
import type {
  DocumentCreatedEventData,
  DocumentSentEventData,
  DocumentViewedEventData,
  DocumentSignedEventData,
  DocumentCompletedEventData,
  DocumentFailedEventData,
  DocumentVoidedEventData,
  DocumentDeclinedEventData,
  TestEventData,
} from '@korala/api-client';
```

**document_created**
```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "event": "document_created",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "documentId": "doc_xyz789",
    "name": "contract.pdf",
    "status": "draft",
    "sandbox": false
  }
}
```

**document_sent**
```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "event": "document_sent",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "documentId": "doc_xyz789",
    "name": "contract.pdf",
    "signerCount": 2,
    "sandbox": false
  }
}
```

**document_viewed**
```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "event": "document_viewed",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "documentId": "doc_xyz789",
    "signerId": "signer_abc123",
    "signerEmail": "john@example.com",
    "signerExternalId": "ext_123",
    "sandbox": false
  }
}
```

**document_signed**
```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "event": "document_signed",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "documentId": "doc_xyz789",
    "signerId": "signer_abc123",
    "signerEmail": "john@example.com",
    "signerExternalId": "ext_123",
    "allSigned": false,
    "sandbox": false
  }
}
```

**document_completed**
```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "event": "document_completed",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "documentId": "doc_xyz789",
    "name": "contract.pdf",
    "completedAt": "2024-01-15T10:30:00Z",
    "signerCount": 2,
    "sandbox": false
  }
}
```

**document_failed**
```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "event": "document_failed",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "documentId": "doc_xyz789",
    "error": "Signing provider unavailable",
    "sandbox": false
  }
}
```

**document_voided**
```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "event": "document_voided",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "documentId": "doc_xyz789",
    "name": "contract.pdf",
    "sandbox": false
  }
}
```

**document_declined**
```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "event": "document_declined",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "documentId": "doc_xyz789",
    "signerId": "signer_abc123",
    "signerEmail": "john@example.com",
    "signerExternalId": "ext_123",
    "reason": "Terms not acceptable",
    "sandbox": false
  }
}
```

## Verifying Signatures

Korala signs all webhook payloads with HMAC-SHA256. Verify the signature on each delivery.

### Signature Headers

| Header | Description |
|--------|-------------|
| `X-Signature` | HMAC-SHA256 signature |
| `X-Timestamp` | Unix timestamp when sent |
| `X-Webhook-Event` | Event type (e.g. `document_signed`) |

### Verification Process

    ```typescript
    import crypto from 'crypto';
    import express from 'express';

    const WEBHOOK_SECRET = 'your-webhook-secret';

    app.post('/webhooks/korala', express.raw({ type: 'application/json' }), (req, res) => {
      const signature = req.headers['x-signature'] as string;
      const timestamp = req.headers['x-timestamp'] as string;
      const body = req.body.toString();

      // Verify timestamp is within 5 minutes
      const now = Math.floor(Date.now() / 1000);
      if (Math.abs(now - parseInt(timestamp)) > 300) {
        return res.status(401).json({ error: 'Timestamp too old' });
      }

      // Compute expected signature
      const message = `${timestamp}.${body}`;
      const expectedSignature = crypto
        .createHmac('sha256', WEBHOOK_SECRET)
        .update(message)
        .digest('hex');

      // Compare signatures
      if (!crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
      )) {
        return res.status(401).json({ error: 'Invalid signature' });
      }

      // Process the webhook
      const event = JSON.parse(body);
      console.log(`Received ${event.event} for document ${event.data.documentId}`);

      // Always respond quickly
      res.status(200).json({ received: true });
    });
    ```

## Retry Policy

Korala retries failed webhook deliveries:

| Attempt | Delay |
|---------|-------|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |

A delivery counts as failed if:
- Your endpoint returns a non-2xx status code
- The request times out (30 seconds)
- A network error occurs

## Managing Webhooks

### List Webhooks

    ```typescript
    const webhooks = await korala.webhooks.list();

    for (const webhook of webhooks) {
      console.log(`${webhook.url}: ${webhook.events.join(', ')}`);
    }
    ```

### Update Webhook

    ```typescript
    await korala.webhooks.update(webhookId, {
      url: 'https://your-app.com/webhooks/v2/korala',
      events: ['document_completed'],
    });
    ```

### Delete Webhook

    ```typescript
    await korala.webhooks.delete(webhookId);
    ```

### View Delivery History

    ```typescript
    const deliveries = await korala.webhooks.deliveries(webhookId);

    for (const delivery of deliveries) {
      console.log(`${delivery.eventType}: ${delivery.status}`);
      console.log(`  Response: ${delivery.responseCode}`);
      console.log(`  Attempts: ${delivery.attemptCount}`);
    }
    ```

## Best Practices

1. **Verify signatures** - Reject any delivery that fails verification
2. **Respond quickly** - Return 200 within 5 seconds, process async
3. **Handle duplicates** - Use `eventId` for idempotency
4. **Use HTTPS** - Korala only sends webhooks to HTTPS endpoints
5. **Monitor failures** - Set up alerts for repeated delivery failures

## Testing Webhooks

Use tools like [webhook.site](https://webhook.site) or [ngrok](https://ngrok.com) to test webhooks during development:

```bash
# Expose local server
ngrok http 3000

# Use the ngrok URL when creating webhooks
# https://abc123.ngrok.io/webhooks/korala
```
