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!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:
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:
{
"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
}
}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
}
}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
| 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
- Verify signatures - Reject any delivery that fails verification
- Respond quickly - Return 200 within 5 seconds, process async
- Handle duplicates - Use
eventIdfor idempotency - Use HTTPS - Korala only sends webhooks to HTTPS endpoints
- 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