Webhooks
Receive real-time notifications when key events occur in your Kruncher account. Register up to 5 webhook URLs and choose, for each one, which events it receives.
Overview
Webhooks allow your application to be notified immediately when:
- A company analysis finishes processing
- A fund data-room analysis finishes processing
- A project’s status or stage changes
Instead of polling the API repeatedly, webhooks push event notifications directly to your server.
Available Events
analysisCompleted
Company analysis completed. Triggered when a company report finishes processing and its
results are available. (The event code stays analysisCompleted for backwards compatibility.)
Payload:
{
"event": "analysisCompleted",
"data": {
"projectId": "521a93a6-091d-4943-ba13-7c1a654a14ae",
"analysisId": "f8e7d6c5-b4a3-2f1e-0d9c-8b7a6f5e4d3c",
"projectName": "Tesla Motors",
"kruncherEntityCompanyId": "04cc17f7-57d9-47ad-ba61-11912af9c027"
}
}When to use: Automatically fetch and process company analysis results when they’re ready.
changedStatus
Triggered when a project’s status or investment stage changes.
Payload:
{
"event": "changedStatus",
"data": {
"projectId": "521a93a6-091d-4943-ba13-7c1a654a14ae",
"analysisId": "f8e7d6c5-b4a3-2f1e-0d9c-8b7a6f5e4d3c",
"newStatusId": "9c1f2a3b-4d5e-4f60-8a71-b2c3d4e5f607",
"newStatusname": "Diligence",
"kruncherEntityCompanyId": "04cc17f7-57d9-47ad-ba61-11912af9c027",
"mappings": [
{ "externalId": "crm-4821", "source": "affinity" }
],
"projectName": "Tesla Motors"
}
}| Field | Type | Description |
|---|---|---|
projectId | string | The project whose status changed |
analysisId | string/null | Its latest analysis |
newStatusId | string | Id of the new status |
newStatusname | string | Name of the new status |
kruncherEntityCompanyId | string/null | The Kruncher entity behind the project |
mappings | array | Your external-system mappings for that entity |
projectName | string | Project name |
When to use: Update your internal systems when projects move to new stages.
fundAnalysisCompleted
Fund analysis completed. Triggered when a fund data-room analysis finishes processing —
that is, when the run reaches status: completed. Runs that fail are not sent; poll
Fund Detail if you need to see error states.
Payload:
{
"event": "fundAnalysisCompleted",
"data": {
"fundExtractionId": "3f1c9a2e-7b4d-4e1a-9c2f-5d6e7f8a9b0c",
"fundId": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b",
"fundName": "Northwind Growth Fund II",
"status": "completed",
"refDate": "2026-06-30",
"fundstage": {
"id": "1a2b3c4d-0000-4000-8000-000000000001",
"name": "Screening",
"type": "screening"
},
"companiesCount": 14
}
}| Field | Type | Description |
|---|---|---|
fundExtractionId | string | The completed run. Pass it to every fund read endpoint |
fundId | string | The fund the run belongs to |
fundName | string | Fund name |
status | string | Always completed |
refDate | string/null | Reporting date of the documents (YYYY-MM-DD) |
fundstage | object/null | The fund’s stage: id, name, type |
companiesCount | integer | Portfolio companies found in the data room |
When to use: Pull the fund once it is ready. Fetch the details with:
curl -X GET "https://api.kruncher.ai/api/integrationfund/fund?fundExtractionId=3f1c9a2e-7b4d-4e1a-9c2f-5d6e7f8a9b0c" \
-H "Authorization: YOUR_API_KEY_HERE"See Funds for the full fund API.
Getting Started
1. Create a Webhook Endpoint
Your endpoint must:
- Accept POST requests
- Be accessible from the internet (HTTPS required in production)
- Return HTTP 200 within 30 seconds
- Validate webhook authenticity
Node.js Example
const express = require('express');
const app = express();
app.use(express.json());
// Webhook endpoint
app.post('/webhooks/kruncher', (req, res) => {
const { event, data } = req.body;
console.log(`Received webhook: ${event}`);
console.log('Data:', data);
// Handle the event
switch (event) {
case 'analysisCompleted':
handleAnalysisCompleted(data);
break;
case 'changedStatus':
handleStatusChanged(data);
break;
case 'fundAnalysisCompleted':
handleFundAnalysisCompleted(data);
break;
}
// Acknowledge receipt
res.status(200).json({ success: true });
});
function handleAnalysisCompleted(data) {
console.log(`Company analysis ${data.analysisId} completed for ${data.projectName}`);
// Fetch full analysis results and process
}
function handleStatusChanged(data) {
console.log(`${data.projectName} status changed to ${data.newStatusname}`);
// Update your system
}
function handleFundAnalysisCompleted(data) {
console.log(`Fund ${data.fundName} analysed: ${data.companiesCount} companies at ${data.refDate}`);
// Fetch the run with GET /api/integrationfund/fund?fundExtractionId=...
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});Python Example
from flask import Flask, request, jsonify
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.route('/webhooks/kruncher', methods=['POST'])
def handle_webhook():
data = request.json
event = data.get('event')
event_data = data.get('data')
logging.info(f"Received webhook: {event}")
logging.info(f"Data: {event_data}")
# Handle events
if event == 'analysisCompleted':
handle_analysis_completed(event_data)
elif event == 'changedStatus':
handle_status_changed(event_data)
elif event == 'fundAnalysisCompleted':
handle_fund_analysis_completed(event_data)
# Acknowledge receipt
return jsonify({'success': True}), 200
def handle_analysis_completed(data):
project_id = data['projectId']
analysis_id = data['analysisId']
project_name = data['projectName']
logging.info(f"Company analysis {analysis_id} completed for {project_name}")
# Fetch full analysis and process
def handle_status_changed(data):
project_name = data['projectName']
new_status = data['newStatusname']
logging.info(f"{project_name} status changed to {new_status}")
# Update system
def handle_fund_analysis_completed(data):
fund_extraction_id = data['fundExtractionId']
fund_name = data['fundName']
logging.info(f"Fund {fund_name} analysed ({data['companiesCount']} companies)")
# Fetch with GET /api/integrationfund/fund?fundExtractionId=<fund_extraction_id>
if __name__ == '__main__':
app.run(port=5000)2. Register Your Webhook URL
Use the webhook configuration endpoint to register your webhook.
Webhook Configuration
The simplest way to register a webhook is in the Kruncher app: Settings → Integrations. Set the URL, tick the events you want, and save.
Multiple webhooks
A workspace can register up to 5 webhooks. Each has its own URL and its own set of events, and every webhook subscribed to an event receives it. Typical setups:
- one URL for company events (
analysisCompleted,changedStatus) and another for fund events (fundAnalysisCompleted); - the same event sent to several systems — for example your CRM and a data warehouse.
Every delivery of an event carries the same body, whichever webhook receives it. A URL can be registered only once — to change which events it receives, edit that webhook.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/integration/webhooks | List your webhooks, oldest first, with maxWebhooks |
| POST | /api/integration/webhooks | Add a webhook. Returns Error215 once you have 5, Error217 if the URL is already registered |
| PUT | /api/integration/webhooks/{id} | Update one webhook; send only the fields you change |
| DELETE | /api/integration/webhooks/{id} | Delete one webhook |
curl -X POST "https://api.kruncher.ai/api/integration/webhooks" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/kruncher-funds",
"fundAnalysisCompleted": true
}'List response:
{
"metadata": { "code": "1000", "title": "Successful", "description": "" },
"data": {
"maxWebhooks": 5,
"webhooks": [
{
"id": "6f0c1d2e-...",
"url": "https://your-server.com/webhooks/kruncher",
"analysisCompleted": true,
"analysisChangedStatus": true,
"fundAnalysisCompleted": false
},
{
"id": "9a8b7c6d-...",
"url": "https://your-server.com/webhooks/kruncher-funds",
"analysisCompleted": false,
"analysisChangedStatus": false,
"fundAnalysisCompleted": true
}
]
}
}A PUT or DELETE on an id that is not one of your webhooks returns Error216.
The single-webhook endpoints below (/api/integration/webhook) keep working exactly as before for one webhook: GET and PUT read and update your oldest webhook, and POST creates it if you have none or updates it if you do. Use /api/integration/webhooks to add a second one.
These configuration endpoints do not accept an Integration API key. Unlike the rest of the Integration API,
/api/integration/webhooks,/api/integration/webhookand/api/integration/webhook/historyrequire a signed-in Kruncher session — an Auth0 access token sent asAuthorization: Bearer <token>, the same token the web app uses. An API key returns401. Everything else on this page — the delivery payloads and the endpoints you call to fetch full data — works with your API key as normal.
Get Current Configuration
GET https://api.kruncher.ai/api/integration/webhook
Returns your current webhook configuration.
curl -X GET "https://api.kruncher.ai/api/integration/webhook" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response:
{
"metadata": {
"code": "1000",
"title": "Successful",
"description": ""
},
"data": {
"id": "webhook_abc123",
"url": "https://your-server.com/webhooks/kruncher",
"analysisCompleted": true,
"analysisChangedStatus": true,
"fundAnalysisCompleted": true,
"createdAt": "2024-01-10T14:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
}Create/Update Webhook Configuration
POST/PUT https://api.kruncher.ai/api/integration/webhook
Create a new webhook or update existing configuration.
cURL
curl -X POST "https://api.kruncher.ai/api/integration/webhook" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/kruncher",
"analysisCompleted": true,
"analysisChangedStatus": true,
"fundAnalysisCompleted": true
}'JavaScript
const API_KEY = "YOUR_API_KEY_HERE";
const BASE_URL = "https://api.kruncher.ai/api";
// ACCESS_TOKEN is an Auth0 access token from a signed-in session,
// not an Integration API key — see the note above.
async function createWebhook(webhookUrl, events, accessToken) {
const payload = {
url: webhookUrl,
analysisCompleted: events.includes('analysisCompleted'),
analysisChangedStatus: events.includes('changedStatus'),
fundAnalysisCompleted: events.includes('fundAnalysisCompleted')
};
const response = await fetch(`${BASE_URL}/integration/webhook`, {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`Failed to create webhook: ${response.statusText}`);
}
return await response.json();
}
// Usage
try {
const result = await createWebhook(
"https://your-server.com/webhooks/kruncher",
["analysisCompleted", "changedStatus", "fundAnalysisCompleted"],
ACCESS_TOKEN
);
console.log("✓ Webhook created:", result.data);
} catch (error) {
console.error("✗ Error:", error.message);
}Python
import requests
# An Auth0 access token from a signed-in session, not an Integration API key.
ACCESS_TOKEN = "YOUR_ACCESS_TOKEN"
BASE_URL = "https://api.kruncher.ai/api"
def create_webhook(webhook_url, events):
"""Create or update webhook configuration."""
headers = {
"Authorization": f"Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"url": webhook_url,
"analysisCompleted": "analysisCompleted" in events,
"analysisChangedStatus": "changedStatus" in events,
"fundAnalysisCompleted": "fundAnalysisCompleted" in events
}
response = requests.post(
f"{BASE_URL}/integration/webhook",
json=payload,
headers=headers
)
response.raise_for_status()
return response.json()
# Usage
try:
result = create_webhook(
"https://your-server.com/webhooks/kruncher",
["analysisCompleted", "changedStatus", "fundAnalysisCompleted"]
)
print(f"✓ Webhook created: {result['data']}")
except Exception as error:
print(f"✗ Error: {error}")Request Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Your webhook endpoint (must be HTTPS in production) |
analysisCompleted | boolean | No | Receive company analysis completion notifications (default: false) |
analysisChangedStatus | boolean | No | Receive project status change notifications (default: false) |
fundAnalysisCompleted | boolean | No | Receive fund analysis completion notifications (default: false) |
The same three events are toggles in the Kruncher app under Settings → API Integration → Webhook.
Response
{
"metadata": {
"code": "1000",
"title": "Successful",
"description": ""
},
"data": {
"id": "webhook_abc123",
"url": "https://your-server.com/webhooks/kruncher",
"analysisCompleted": true,
"analysisChangedStatus": true,
"fundAnalysisCompleted": true,
"createdAt": "2024-01-15T10:30:00Z"
}
}Fetching Full Data
Webhook payloads contain only essential IDs. Use your API key to fetch complete details:
Get Project Details
curl -X GET "https://api.kruncher.ai/api/integration/projects?id=521a93a6-091d-4943-ba13-7c1a654a14ae" \
-H "Authorization: YOUR_API_KEY_HERE"Get Analysis Details
curl -X GET "https://api.kruncher.ai/api/integration/analysis/detail?analysisId=f8e7d6c5-b4a3-2f1e-0d9c-8b7a6f5e4d3c" \
-H "Authorization: YOUR_API_KEY_HERE"Get Fund Run Details
curl -X GET "https://api.kruncher.ai/api/integrationfund/fund?fundExtractionId=3f1c9a2e-7b4d-4e1a-9c2f-5d6e7f8a9b0c" \
-H "Authorization: YOUR_API_KEY_HERE"For the whole report in one call, use Full Report instead:
curl -X GET "https://api.kruncher.ai/api/integration/project/fullreport?projectId=521a93a6-091d-4943-ba13-7c1a654a14ae" \
-H "Authorization: YOUR_API_KEY_HERE"Complete Event Handler
class WebhookHandler {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = "https://api.kruncher.ai/api";
}
async getProjectDetails(projectId) {
const response = await fetch(
`${this.baseUrl}/integration/projects?id=${projectId}`,
{
headers: { "Authorization": `${this.apiKey}` }
}
);
return await response.json();
}
async getAnalysisDetails(analysisId) {
const response = await fetch(
`${this.baseUrl}/integration/analysis/detail?analysisId=${analysisId}`,
{
headers: { "Authorization": `${this.apiKey}` }
}
);
return await response.json();
}
async handleAnalysisCompleted(data) {
const { projectId, analysisId } = data;
// Fetch full details
const [project, analysis] = await Promise.all([
this.getProjectDetails(projectId),
this.getAnalysisDetails(analysisId)
]);
console.log("Project:", project.data);
console.log("Analysis:", analysis.data);
// Process analysis results
this.processAnalysis(project.data, analysis.data);
}
async handleStatusChanged(data) {
const { projectId, newStatusname } = data;
// Fetch updated project details
const project = await this.getProjectDetails(projectId);
console.log(`Project ${project.data.name} now in ${newStatusname} stage`);
// Update your system
this.updateSystem(project.data);
}
async getFundDetails(fundExtractionId) {
const response = await fetch(
`${this.baseUrl}/integrationfund/fund?fundExtractionId=${fundExtractionId}`,
{
headers: { "Authorization": `${this.apiKey}` }
}
);
return await response.json();
}
async handleFundAnalysisCompleted(data) {
const { fundExtractionId } = data;
// Fetch the run summary, then the view you need (workbook, overview, assets, ...)
const fund = await this.getFundDetails(fundExtractionId);
console.log("Fund:", fund.data.name, fund.data.companiesCount, "companies");
}
processAnalysis(project, analysis) {
// Your custom processing logic
console.log(`Processing analysis for ${project.name}`);
}
updateSystem(project) {
// Your custom update logic
console.log(`Updating system for ${project.name}`);
}
}
// Usage
const handler = new WebhookHandler("YOUR_API_KEY_HERE");
app.post('/webhooks/kruncher', async (req, res) => {
const { event, data } = req.body;
try {
if (event === 'analysisCompleted') {
await handler.handleAnalysisCompleted(data);
} else if (event === 'changedStatus') {
await handler.handleStatusChanged(data);
} else if (event === 'fundAnalysisCompleted') {
await handler.handleFundAnalysisCompleted(data);
}
res.status(200).json({ success: true });
} catch (error) {
console.error("Webhook processing error:", error);
res.status(500).json({ success: false, error: error.message });
}
});Webhook History
View webhook delivery history for debugging and monitoring.
Get Webhook History
GET https://api.kruncher.ai/api/integration/webhook/history
Returns the 10 most recent webhook deliveries, ordered by most recent first. No query parameters are accepted.
cURL
curl -X GET "https://api.kruncher.ai/api/integration/webhook/history" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"JavaScript
async function getWebhookHistory() {
const response = await fetch(
`${BASE_URL}/integration/webhook/history`,
{
// Session token, not an Integration API key
headers: { "Authorization": `Bearer ${ACCESS_TOKEN}` }
}
);
return await response.json();
}
// Usage
const history = await getWebhookHistory();
console.log("Recent deliveries:", history.data);Python
def get_webhook_history():
"""Get webhook delivery history."""
response = requests.get(
f"{BASE_URL}/integration/webhook/history",
headers={"Authorization": f"{API_KEY}"}
)
response.raise_for_status()
return response.json()
# Usage
history = get_webhook_history()
for delivery in history['data']:
print(f"{delivery['event']}: {delivery['status']}")Response Structure
{
"metadata": {
"code": "1000",
"title": "Successful",
"description": ""
},
"data": [
{
"url": "https://your-server.com/webhooks/kruncher",
"event": "analysisCompleted",
"status": "Success",
"statusDescription": null,
"createdAt": "2024-01-15T10:30:00Z"
},
{
"event": "changedStatus",
"status": "Error Response",
"statusDescription": "Connection timeout after 30 seconds",
"createdAt": "2024-01-15T09:45:00Z"
}
]
}History Fields
| Field | Type | Description |
|---|---|---|
url | string | The URL the delivery was sent to |
event | string | Event type (analysisCompleted, changedStatus or fundAnalysisCompleted) |
status | string | "Success", "Error Request", or "Error Response" |
statusDescription | string/null | Error details if applicable |
createdAt | string | ISO 8601 timestamp of the event |
Best Practices
Webhook Security
-
Validate HTTPS
- Require HTTPS in production
- Use valid SSL certificates
- Never use self-signed certs in production
-
Verify Authenticity
// Check webhook comes from Kruncher const apiKey = req.headers['x-kruncher-api-key']; if (apiKey !== process.env.KRUNCHER_API_KEY) { return res.status(401).json({ error: 'Unauthorized' }); } -
Handle Duplicates
// Store delivery IDs to prevent duplicate processing const processedDeliveries = new Set(); app.post('/webhooks/kruncher', (req, res) => { const deliveryId = req.headers['x-delivery-id']; if (processedDeliveries.has(deliveryId)) { return res.status(200).json({ success: true }); // Already processed } // Process webhook processedDeliveries.add(deliveryId); });
Reliability
-
Quick Response
- Acknowledge receipt immediately (200 OK)
- Process asynchronously in background
- Don’t perform long operations synchronously
-
Error Handling
app.post('/webhooks/kruncher', async (req, res) => { // Always respond quickly res.status(200).json({ success: true }); // Process asynchronously setImmediate(async () => { try { await processWebhook(req.body); } catch (error) { console.error('Webhook processing failed:', error); // Implement retry logic or alerting } }); }); -
Monitoring
- Log all webhook deliveries
- Monitor delivery failures
- Set up alerts for repeated failures
- Check webhook history regularly
Performance
-
Batch Processing
from collections import defaultdict pending_tasks = defaultdict(list) def handle_webhook(event, data): # Group related tasks project_id = data['projectId'] pending_tasks[project_id].append({ 'event': event, 'data': data }) # Process after delay to batch schedule_batch_processing(project_id, delay=5) -
Caching
- Cache project/analysis details locally
- Reduce API calls to fetch full data
- Refresh cache on status changes
Common Patterns
Pattern 1: Auto-Process Analysis Results
async function handleAnalysisComplete(data) {
const handler = new WebhookHandler(API_KEY);
// Get full analysis
const analysis = await handler.getAnalysisDetails(data.analysisId);
// Process results
updateDatabase({
projectId: data.projectId,
analysisResults: analysis.data,
processedAt: new Date()
});
// Notify team
sendSlackNotification(
`Analysis complete for ${data.projectName}`,
analysis.data.summary
);
}Pattern 2: Update External Systems
def handle_status_change(data):
"""Sync status changes to external CRM."""
project_id = data['projectId']
new_status = data['newStatus']
# Map Kruncher status to external CRM status
crm_status = map_status(new_status)
# Update CRM
crm_client.update_deal(
deal_id=project_id,
status=crm_status
)
# Log change
logger.info(f"Updated {project_id} to {crm_status}")Pattern 3: Delayed Processing
class WebhookQueue {
constructor() {
this.queue = [];
}
async add(event, data) {
this.queue.push({ event, data, timestamp: Date.now() });
// Process queue after batch window
if (this.queue.length === 1) {
setTimeout(() => this.processBatch(), 5000);
}
}
async processBatch() {
const batch = this.queue.splice(0);
for (const item of batch) {
await this.handleEvent(item.event, item.data);
}
}
}Error Handling
Handling Delivery Failures
async function getFailedDeliveries() {
const response = await fetch(
`${BASE_URL}/integration/webhook/history`,
{ headers: { "Authorization": `${API_KEY}` } }
);
const history = await response.json();
// Check for pattern of failures
const failures = history.data.filter(d => d.status !== 'Success');
if (failures.length > 5) {
console.error("⚠️ Multiple webhook failures detected");
console.error("Check your endpoint accessibility");
}
return failures;
}Retry Logic
def handle_webhook_with_retry(event, data, max_retries=3):
"""Process webhook with retry logic."""
for attempt in range(max_retries):
try:
if event == 'analysisCompleted':
process_analysis(data)
elif event == 'changedStatus':
process_status_change(data)
return True # Success
except Exception as error:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff
time.sleep(wait_time)
else:
log_failure(event, data, error)
raiseTesting Your Webhook
Local Testing with ngrok
# Start your local server
npm start # or python app.py
# Expose to internet
ngrok http 3000
# Register webhook with ngrok URL
# https://abc123.ngrok.io/webhooks/kruncher
# View requests
ngrok web # Opens debugging interfaceManual Testing
# Send test webhook
curl -X POST "https://your-server.com/webhooks/kruncher" \
-H "Content-Type: application/json" \
-d '{
"event": "analysisCompleted",
"data": {
"projectId": "test-project-id",
"analysisId": "test-analysis-id",
"projectName": "Test Project"
}
}'Troubleshooting
”Connection timeout” Error
- Check endpoint is publicly accessible
- Verify firewall allows incoming connections
- Ensure endpoint returns response within 30 seconds
- Check server logs for errors
Webhook Not Triggering
- Verify webhook is active in settings
- Check event subscription is enabled
- Verify webhook URL is correct
- Check webhook history for errors
Duplicate Events
- Implement idempotency (check delivery ID)
- Store processed event IDs
- Handle gracefully on retry
Related Endpoints
- Funds - Fund runs, the
fundAnalysisCompletedevent and the endpoints to fetch a fund - Fund Detail - Fetch a fund run after
fundAnalysisCompleted - Get Project Details - Fetch complete project data
- Get Analysis Details - Fetch complete analysis data
- List Projects - View all projects
- Search Projects - Find specific projects
Need Help?
- Check webhook history for delivery status and errors
- Verify endpoint is accessible and returning 200 OK
- Test manually with curl to debug issues
- Use ngrok for local development and testing