Skip to Content
API setupWebhooks

Webhooks

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

Event Types

EventDescription
document_createdDocument was created
document_sentDocument was sent for signing
document_viewedA signer opened the document
document_signedA signer completed their signature
document_completedAll signers finished, document is sealed
document_voidedDocument was cancelled
document_declinedA signer declined to sign

Signing packet events

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

EventDescription
signing_packet.sentKorala froze the manifest and sent the packet
signing_packet.viewedThe recipient opened the packet
signing_packet.item_signedKorala signed one packet item
signing_packet.recipient_completedThe recipient signed all packet items
signing_packet.partially_completedSome selected items could not complete
signing_packet.voidedThe sender voided the packet
signing_packet.expiredThe packet passed its expiration time

Document events still fire for packet items. When the recipient first opens the packet, you receive document_viewed for each document they had not opened yet, alongside signing_packet.viewed. When Korala signs an item, you receive document_signed for that document and signing_packet.item_signed for the packet, so one signature can reach you twice. Both document events carry packetId and packetItemId; a document viewed or signed any other way has neither. 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

const webhook = await korala.webhooks.create({ url: 'https://your-app.com/webhooks/korala', events: ['document_completed', 'document_signed'], mode: 'live', // match the API credential mode }); console.log(`Webhook ID: ${webhook.id}`); console.log(`Secret: ${webhook.secret}`); // Save this for verification!

Test and Live Mode

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

ModeReceives
live (default)Events from live documents, sessions and workflows
testEvents from test documents, sessions and workflows

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.

Subscription mode cannot be changed after creation. Register separate endpoints for each mode; API keys can manage only subscriptions in their own mode. Dashboard JWTs can manage both. Use credentials matching each endpoint below:

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 tells you which environment an event came from, so a single handler can serve both endpoints if you prefer.

Webhook Payload

All webhooks follow this format:

{ "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:

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

document_created

{ "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

{ "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

{ "eventId": "550e8400-e29b-41d4-a716-446655440000", "event": "document_viewed", "timestamp": "2024-01-15T10:30:00Z", "data": { "documentId": "doc_xyz789", "signerId": "signer_abc123", "signerEmail": "[email protected]", "signerExternalId": "ext_123", "sandbox": false } }

A view recorded when the recipient opened a signing packet adds "packetId" and "packetItemId" to data.

document_signed

{ "eventId": "550e8400-e29b-41d4-a716-446655440000", "event": "document_signed", "timestamp": "2024-01-15T10:30:00Z", "data": { "documentId": "doc_xyz789", "signerId": "signer_abc123", "signerEmail": "[email protected]", "signerExternalId": "ext_123", "allSigned": false, "sandbox": false } }

A signature made through a signing packet adds "packetId" and "packetItemId" to data, and one applied by bulk signing adds "bulkSign": true.

document_completed

{ "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

{ "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

{ "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

{ "eventId": "550e8400-e29b-41d4-a716-446655440000", "event": "document_declined", "timestamp": "2024-01-15T10:30:00Z", "data": { "documentId": "doc_xyz789", "signerId": "signer_abc123", "signerEmail": "[email protected]", "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

HeaderDescription
X-SignatureHMAC-SHA256 signature
X-TimestampUnix timestamp when sent
X-Webhook-EventEvent type (e.g. document_signed)

Verification Process

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:

AttemptDelay
1Immediate
21 minute
35 minutes
415 minutes
51 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

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

Update Webhook

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

Delete Webhook

await korala.webhooks.delete(webhookId);

View Delivery History

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  or ngrok  to test webhooks during development:

# Expose local server ngrok http 3000 # Use the ngrok URL when creating webhooks # https://abc123.ngrok.io/webhooks/korala
Last updated on