API
Webhooks
Signed HTTPS events for Signals API searches and watches. The Inbox and the lead table do not push.
FounderMCP can POST events to an HTTPS URL you choose, for the Signals API. That is the public webhook surface. There is no webhook for a new lead in the FounderMCP app, a reply in the Inbox, a template edit, or a call to the assistant MCP server at https://geteem.com/api/mcp.
The endpoints below use the same key and the same gate as the Signals API. When that API is not open for your organization, they answer 404, the same body as an unknown path under /api/v1. The machine-readable contract is /openapi.json.
Register an endpoint
POST https://geteem.com/api/v1/webhook_endpoints with Authorization: Bearer gtk_… and a required Idempotency-Key header.
curl -X POST "https://geteem.com/api/v1/webhook_endpoints" \
-H "Authorization: Bearer $SIMBA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f1c0b2e-6a7d-4e8f-9c1a-2b3d4e5f6071" \
-d '{
"url": "https://example.com/hooks/foundermcp",
"description": "Production",
"events": ["search.completed", "watch.leads_delivered"]
}'
The response is 201 and includes secret (whsec_…) once. Store it. Later reads of the endpoint do not return it. Omit events to subscribe to every type below. ping is not in that list; the test call sends it anyway.
The URL has to be https on port 443, a public hostname, with no user name, password or IP address. Hosts that resolve to a private or reserved address are rejected. The description is at most 200 characters. An organization can have 5 endpoints.
GET https://geteem.com/api/v1/webhook_endpointslists them, without the secret.GET …/webhook_endpoints/{id}reads one. Ids look likewhe_….DELETE …/webhook_endpoints/{id}removes one. A second delete of the same id returns the same object.POST …/webhook_endpoints/{id}/testsends apingimmediately. Your URL must answer 2xx within 10 seconds. Redirects are not followed. A successful test turns a disabled endpoint back on. Watches that were paused stay paused until you resume them.
Creating, listing and testing an endpoint does not spend credits.
Events
A delivery is one JSON object:
{
"id": "evt_…",
"object": "event",
"type": "search.completed",
"created_at": "2026-10-22T12:00:00.000Z",
"api_version": "2026-10-04",
"data": {}
}
Subscribe with these type values:
search.completed— the search finished.datacarries the search and up to 25 rows or leads.has_moremeans the rest is on the pull URL.search.failed— the search failed. Nothing was charged for a failed search.search.requires_action— an event name matched more than one event. The user picks one, then the search continues.watch.leads_delivered— a watch run delivered new leads (up to 25 in the event).watch.paused— a watch paused.watch.failed_run— a watch run failed.
Set webhook_endpoint_id on the search or the watch that should notify that endpoint. An endpoint only receives types it subscribed to, and only while its status is enabled.
Delivery does not decide credits. The rows are already available from the GET endpoints. The same event is not queued twice. Delivery is at-least-once: dedupe on id, which is also the Simba-Event-Id header.
Verify the signature
Every POST has these headers:
User-Agent: Simba-Webhooks/1.0Simba-Event-Id— theidin the bodySimba-Event-Type— thetypeSimba-Signature: t=<unix seconds>,v1=<hex>
v1 is HMAC-SHA256 of <t>.<raw body>, keyed with the endpoint secret, hex-encoded. The signed bytes are the raw body, so read the body before you parse JSON. Reject a timestamp more than 300 seconds off your clock. Compare the hex in constant time. The header can carry more than one v1; any one match is enough.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifySimbaSignature(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {
const parts = new Map();
for (const piece of String(header).split(',')) {
const [key, ...rest] = piece.trim().split('=');
if (key && rest.length) parts.set(key, rest.join('='));
}
const timestamp = Number(parts.get('t'));
if (!Number.isInteger(timestamp) || Math.abs(nowSeconds - timestamp) > 300) return false;
const expected = createHmac('sha256', secret).update(timestamp + '.' + rawBody).digest('hex');
const given = parts.get('v1') ?? '';
if (!/^[0-9a-f]{64}$/i.test(given)) return false;
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(given, 'hex'));
}
Retries
A 2xx within 10 seconds is delivered. Anything else — timeout, connection or TLS error, a redirect, a 4xx or a 5xx — is a failed attempt. FounderMCP tries again after 2^attempt minutes, and waits at most 60 minutes between attempts, up to 8 attempts. After three events in a row use up their attempts, the endpoint is disabled and watches that deliver to it are paused. You resume the watches yourself. A successful test turns the endpoint back on.
Events are kept for 30 days. One event is at most 256 KB; a larger lead list is cut to 25 and has_more tells you to pull the rest.