Guide to Cuuro Webhooks: Features and Best Practices
Cuuro uses AI to run due diligence on companies for investors. You add a target company, start an analysis, and Cuuro works through it in the background. Its webhooks tell your system when a target company has been created and when an analysis starts and finishes, so a deal-flow tool or CRM can pick up the finished analysis without polling.
What are Cuuro webhooks?
Webhooks in Cuuro belong to an API integration. You create one on the integration's Webhooks tab with a URL and the events it should receive, and Cuuro sends each event as a JSON POST. Cuuro states that it uses Svix to send them and links to Svix's guides for verification, so the requests carry Svix's svix-id, svix-timestamp and svix-signature headers.
Cuuro's own webhook page is short. It lists three events and the setup steps, and leaves signing, retries and timeouts to Svix's documentation.
Cuuro webhook features
| Feature | Details |
|---|---|
| Configuration | Settings > Developers, an API integration's Webhooks tab |
| Delivery platform | Svix |
| Signature | Standard Webhooks HMAC-SHA256 |
| Signature header | svix-signature, with svix-id and svix-timestamp (Svix defaults; Cuuro's page names none) |
| Secret | Svix endpoint secret. Location and format not documented by Cuuro |
| Body | JSON, an ID only |
| Event routing | Body field event |
| Source IPs | Svix's published webhook IPs |
| Retries and timeout | Not documented |
Events and payloads
| Documented event | Meaning |
|---|---|
startup.created | This event is triggered when a new target company is created. |
analysis.created | This event is triggered when a new analysis is created. |
analysis.completed | This event is triggered when a new analysis is completed. |
Each body names the event and gives one ID in data.id: the company for startup.created, the analysis for the two analysis.* events. Nothing else is included, so fetch the company or analysis from the Cuuro API before you act. This is the analysis.completed sample from the webhook samples library:
{
"data": {
"id": "0191e29a-1644-7972-9466-18ada4a3636e"
},
"event": "analysis.completed"
}
Setting up Cuuro webhooks
- In the Cuuro dashboard, open Settings > Developers and click the API integration you want.
- Open its Webhooks tab and click Create Webhook.
- Enter the endpoint URL and select one or more Events, then click Create.
Cuuro doesn't say where the signing secret appears or what it looks like. Svix endpoint secrets start with whsec_, so look for one on the webhook's entry in the Webhooks tab and ask Cuuro support if it isn't shown. Store it as CUURO_WEBHOOK_SECRET.
For Hookdeck, create a source with the Cuuro type, paste the secret, and use the source URL when you create the webhook. The Hookdeck CLI can then forward events to a local server:
npx hookdeck-cli listen 3000 cuuro --path /webhooks/cuuro
Securing Cuuro webhooks
Follow Svix's manual method: sign <svix-id>.<svix-timestamp>.<raw body> with HMAC-SHA256, keyed with the base64 bytes after whsec_, and compare with each v1, entry in svix-signature. Because Cuuro doesn't document its header names, the function falls back to webhook-* headers if svix-id isn't there, which covers a white-labelled Svix setup:
const crypto = require('node:crypto');
function decodeBase64(value) {
if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
const bytes = Buffer.from(value, 'base64');
return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
? bytes : null;
}
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const prefix = headers['svix-id'] ? 'svix' : 'webhook';
const id = headers[`${prefix}-id`];
const timestamp = headers[`${prefix}-timestamp`];
const signature = headers[`${prefix}-signature`];
if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
!/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
const key = decodeBase64(secret.replace(/^whsec_/, ''));
if (!key) return false;
const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
const received = decodeBase64(candidate);
if (!received) return false;
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The Svix libraries that Cuuro links to also reject timestamps older than five minutes. Hookdeck applies no timestamp tolerance and resends the original svix-timestamp on retries and replays, so use this function, or relax the library's tolerance, when events come through Hookdeck.
An analysis.completed event is the one most integrations act on. The receiver records it and lets a worker download the analysis:
const express = require('express');
// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
try {
const value = JSON.parse(rawBody.toString('utf8'));
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
} catch {
return null;
}
}
// tasks.schedule(kind, id, deliveryId) records the task and resolves once stored.
function createCuuroReceiver(tasks) {
const app = express();
app.post('/webhooks/cuuro', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.CUURO_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
const body = parseJsonObject(req.body);
if (!body) return res.sendStatus(400); // signed, but not a JSON object
const { event, data } = body;
if (!data || typeof data !== 'object') return res.sendStatus(400);
const deliveryId = req.headers['svix-id'] || req.headers['webhook-id'];
try {
if (event === 'analysis.completed') await tasks.schedule('fetch-analysis', data.id, deliveryId);
else if (event === 'startup.created') await tasks.schedule('sync-company', data.id, deliveryId);
} catch {
return res.sendStatus(503);
}
return res.sendStatus(200);
});
return app;
}
When Cuuro posts to a Hookdeck Cuuro source, an invalid signature is answered with 200 and logged in Hookdeck as failed verification. It isn't delivered to this receiver.
Make Cuuro webhooks production-ready. Hookdeck Event Gateway verifies the Svix signature on Cuuro's analysis events and keeps them until your deal-flow service has fetched each analysis.
Cuuro webhook limitations and pain points
Delivery behaviour is undocumented
The Problem: You don't know how long Cuuro waits for a response or whether it retries.
Why It Happens: Cuuro's page covers events and setup only. Svix normally retries failed messages, but Cuuro doesn't say which policy its Svix app uses.
Workarounds:
- Answer
2xxas soon as the event is stored. - Periodically list analyses through the API to catch any completion you missed.
How Hookdeck Can Help: Hookdeck takes the delivery immediately and retries your service on a schedule you set, so you don't depend on an unpublished policy.
The secret isn't described
The Problem: You can't confirm where to find the secret or how to use it from Cuuro's docs.
Why It Happens: Cuuro defers to Svix's documentation and never shows a secret value.
Workarounds:
- Check the webhook in the dashboard for a
whsec_value, and confirm with Cuuro support if there isn't one.
How Hookdeck Can Help: After you paste the secret into a Hookdeck source, the request log shows whether the first real delivery verified, which settles the question.
Payloads carry only an ID
The Problem: Every event needs an API call before you can do anything with it.
Why It Happens: The body is event plus data.id.
Workarounds:
- Fetch from a worker, not the request handler, and cache the analysis once it is complete.
How Hookdeck Can Help: Hookdeck's max delivery rate keeps a batch of completions from turning into a burst of Cuuro API calls.
Best practices
Deduplicate on svix-id
Svix keeps the same message ID across retries. Store it, or the pair of event and data.id, before scheduling work. Webhook idempotency has a worked example.
Subscribe to what you use
If you only need finished analyses, subscribe to analysis.completed alone. Fewer events means fewer API lookups.
Restrict by IP as a second layer
Cuuro suggests accepting only Svix's webhook IPs. Keep signature verification as the main check. If you put Hookdeck in front, your firewall sees Hookdeck's addresses instead.
Conclusion
Cuuro's webhooks tell you when a company or analysis is created or an analysis finishes. The documentation is thin, so verify the Svix signature and fetch the analysis from the API rather than relying on the event alone.
Hookdeck Event Gateway can fill in the delivery guarantees Cuuro doesn't document. Sign up free.