Crutan Developer API
Programmatic access to prospects, page generation, conversion analytics, and real-time webhooks. The same /api/v1 surface documented here powers the official MCP server and every first-party integration.
Introduction
The Crutan API is organized around REST. It accepts JSON request bodies, returns JSON responses, and uses standard HTTP verbs and status codes. All requests are scoped to a single workspace, determined by the API key you authenticate with.
Base URL
https://app.crutan.com/api/v1Replace the host with your own deployment if you are self-hosting. All endpoints in this reference are relative to /api/v1.
Authentication
Authenticate every request with a workspace API key in the Authorization header using the Bearer scheme. Keys are prefixed with crt_ and are created in Settings → API & MCP keys. A key is shown once at creation — store it securely.
curl https://app.crutan.com/api/v1/prospects \
-H "Authorization: Bearer $CRUTAN_API_KEY"402 with { "error": "plan_limit", "reason": "api_access_requires_paid_plan" }. Keys may carry a monthly spend cap (UTC calendar month); calls that would exceed it return spend_cap_exceeded.Conventions
Requests with a body must send Content-Type: application/json. Successful responses return a JSON object keyed by the resource (for example { "prospects": [...] }). List endpoints are capped — request the next window by filtering on created_at.
Errors & status codes
Errors return a JSON object with an error field, and where relevant a machine-readable reason.
{
"error": "plan_limit",
"reason": "api_access_requires_paid_plan",
"upgradeUrl": "/settings#billing"
}| Status | Meaning |
|---|---|
200 | Success. |
400 | Malformed request or validation error (e.g. no_prospects). |
401 | Missing or invalid key (api_key_required, workspace_required). |
402 | Plan limit reached or API access not enabled. |
404 | Resource not found (not_found). |
429 | Rate limited — retry after the Retry-After header. |
Rate limits
Throttled requests receive 429 with a Retry-After header (seconds). Back off and retry; do not loop tightly.
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
{ "error": "rate_limited" }Idempotency
Write endpoints accept an Idempotency-Key header. Re-sending the same key (per workspace) replays the original response instead of creating duplicate records — safe for retries from CRMs and queues.
curl https://app.crutan.com/api/v1/prospects \
-X POST \
-H "Authorization: Bearer $CRUTAN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: import-2026-06-04-batch-17" \
-d '{ "email": "jordan@acme.com", "company_name": "Acme" }'Prospects
List prospects in the workspace, newest first (max 500).
{
"prospects": [
{
"id": "b1f0…",
"email": "jordan@acme.com",
"first_name": "Jordan",
"last_name": "Lee",
"company_name": "Acme",
"company_domain": "acme.com",
"role_title": "VP Sales",
"linkedin_url": null,
"tags": ["q3-outbound"],
"custom_fields": {},
"external_ids": { "hubspot": "501" },
"status": "rendered",
"created_at": "2026-06-04T18:20:11Z"
}
]
}Create or update prospects. Records are upserted on (workspace, email), so this is safe to call repeatedly. Accepts a single object, a bare array, or an object with a prospects array.
| Field | Type | Notes |
|---|---|---|
email | string | Required. Upsert key. |
first_name / last_name | string | Optional. |
company_name / company_domain | string | Optional. |
role_title / linkedin_url | string | Optional. |
tags | string[] | Optional. |
custom_fields / external_ids | object | Optional. Free-form JSON. |
curl https://app.crutan.com/api/v1/prospects \
-X POST \
-H "Authorization: Bearer $CRUTAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prospects": [
{ "email": "jordan@acme.com", "first_name": "Jordan", "company_name": "Acme" },
{ "email": "sam@globex.com", "first_name": "Sam", "company_name": "Globex" }
]
}'{
"prospects": [
{ "id": "b1f0…", "email": "jordan@acme.com", "first_name": "Jordan",
"company_name": "Acme", "status": "pending", "created_at": "2026-06-04T18:20:11Z" }
]
}Fetch one prospect, including the personalized pages generated for them.
{
"prospect": {
"id": "b1f0…",
"email": "jordan@acme.com",
"status": "rendered",
"pages": [
{ "id": "c4a2…", "page_token": "k3f…q9.ab12cd",
"url": "https://go.yourco.com/k3f…q9.ab12cd", "status": "rendered" }
]
}
}Pages
Return the status and public URL of a generated recipient page.
{
"page": {
"id": "c4a2…",
"page_token": "k3f…q9.ab12cd",
"url": "https://go.yourco.com/k3f…q9.ab12cd",
"status": "rendered",
"prospect_id": "b1f0…",
"template_id": "9d77…",
"created_at": "2026-06-04T18:25:02Z"
}
}Analytics
Conversion-first metrics for the workspace plus the hottest leads, ranked by conversions and high-intent signals. Low-value signals (scroll depth, dwell) are intentionally excluded — view them per page in the dashboard.
{
"metrics": {
"pagesLive": 248,
"visited": 173,
"conversions": 41,
"ctaClicks": 96,
"conversionRate": 0.165
},
"hotLeads": [
{ "page_token": "k3f…q9.ab12cd", "prospect_id": "b1f0…",
"visits": 4, "cta_clicks": 3, "conversions": 1,
"high_intent_count": 2, "converted": true,
"last_conversion_at": "2026-06-04T19:02:55Z" }
]
}Leads
List lead submissions (form fills and booked meetings), newest first (max 250). Poll this endpoint, or receive the same data in real time via webhooks.
{
"leads": [
{
"id": "f02c…",
"prospect_id": "b1f0…",
"page_token": "k3f…q9.ab12cd",
"event_type": "form.submitted",
"kind": "lead",
"fields": { "email": "jordan@acme.com", "message": "Let's talk" },
"created_at": "2026-06-04T19:02:55Z"
}
]
}Batches
Create a batch to generate personalized pages for many prospects at once, then queue rendering. API-key calls must pass an exact acknowledgedEstimateCents from your cost estimate (same guardrail as MCP).
Create a draft batch for a template. Omit prospectIds to include up to 5,000 recent prospects in the workspace.
curl https://app.crutan.com/api/v1/batches \
-X POST \
-H "Authorization: Bearer $CRUTAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "9d77…",
"batchName": "June outbound",
"prospectIds": ["b1f0…", "c2a1…"]
}'Queue async generation (default) or pass { "forceSync": true } for a blocking run in smaller batches.
{
"acknowledgedEstimateCents": 2400,
"prospectIds": ["b1f0…"]
}{
"batchId": "a8c1…",
"status": "generating",
"queued": 42,
"mode": "async"
}Events
Cursor-paginated export of conversion events (form submits and bookings). Use when you prefer polling over webhooks. Pass cursor fromnextCursor to page forward; limit defaults to 50 (max 250).
{
"events": [
{
"id": "f02c…",
"event": "form.submitted",
"prospect_id": "b1f0…",
"page_token": "k3f…q9.ab12cd",
"fields": { "email": "jordan@acme.com" },
"delivered_at": "2026-06-04T19:03:01Z",
"created_at": "2026-06-04T19:02:55Z"
}
],
"nextCursor": "eyJjcmVhdGVkX2F0Ijoi…"
}Discovery pages
Discovery pages are personalized follow-up pages generated immediately after a discovery call. The API lets you create a session, submit answers to the AI-surfaced questions, and trigger the page build — all programmatically.
Create a discovery session and kick off AI analysis. Supply any combination of transcript, notes, and solutionMap. The session moves to interview_ready once analysis completes (poll GET /api/discovery/{id} or use the MCP tool).
| Field | Type | Notes |
|---|---|---|
prospectId | uuid | Optional. Pin to an existing prospect. |
prospect | object | Optional. Inline prospect fields if no prospectId. |
transcript | string | Optional. Pasted call transcript. |
notes | string | Optional. Freeform call notes. |
solutionMap | array | Optional. [{ painPoint, solution, proof }] |
exclusions | string | Optional. Topics to keep off the page. |
consentAttested | boolean | Required when uploading a recording. |
{ "sessionId": "d7f3…" }Poll session status and retrieve the AI-generated questions. The status field progresses through draft → ingesting → interview_ready → answered → building → completed.
{
"session": {
"id": "d7f3…",
"status": "interview_ready",
"template_id": null
},
"questions": [
{
"id": "q1a2…",
"question": "How long does implementation take for a team our size?",
"context": "Raised during the call.",
"category": "process",
"ai_suggested_answer": "Typically 2–3 weeks…",
"answer_source": "ai_rag",
"rag_confidence": 0.91,
"status": "open"
}
]
}Persist answers to the AI-generated questions. Accepts the full questions array with your edits.
curl https://app.crutan.com/api/discovery/d7f3… /answers \
-X POST \
-H "Authorization: Bearer $CRUTAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"answers": [
{
"id": "q1a2…",
"question": "How long does implementation take for a team our size?",
"answer": "Typically 2–3 weeks with a dedicated point of contact.",
"status": "answered"
}
]
}'{ "questions": [ /* updated array */ ] }Enqueue async page build (Inngest). Debits one discovery credit when the job completes. Returns 202 Accepted immediately — poll GET /api/discovery/{id} until status is completed and template_id is set.
{ "buildId": "a1b2…", "status": "building" }{
"session": { "status": "completed", "template_id": "9d77…" },
"questions": [ /* … */ ]
}GET /api/billing/entitlements.Webhooks
Register an endpoint to receive events the moment they happen. Crutan signs every delivery, retries with backoff, and records each attempt.
Register an outbound webhook. The signing secret is returned once — store it.
curl https://app.crutan.com/api/v1/connectors \
-X POST \
-H "Authorization: Bearer $CRUTAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"outboundUrl": "https://hooks.yourcrm.com/crutan",
"events": ["form.submitted", "meeting.booked", "lead.intent"]
}'{
"connector": {
"id": "a91b…",
"provider": "custom",
"outbound_url": "https://hooks.yourcrm.com/crutan",
"event_subscriptions": ["form.submitted", "meeting.booked", "lead.intent"],
"status": "active",
"created_at": "2026-06-04T18:00:00Z"
},
"secret": "whsec_…"
}Events
| Event | Fires when |
|---|---|
page.rendered | A personalized page finishes generating. |
form.submitted | A prospect submits a lead form. |
meeting.booked | A meeting is booked from a page. |
lead.intent | A high-intent signal is captured. |
Delivery & signature
Each delivery is an HTTP POST with a JSON body and an X-Crutan-Signature header of the form t=<unix_seconds>,v1=<hex>. The HMAC-SHA256 is computed over {t}.{raw_body} using your connector secret. Always verify against the raw request body before trusting the payload.
POST /crutan HTTP/1.1
Host: hooks.yourcrm.com
Content-Type: application/json
X-Crutan-Signature: t=1717524175,v1=9f8c2b…e41
{
"event": "form.submitted",
"workspace_id": "7c1d…",
"prospect": { "email": "jordan@acme.com", "company_name": "Acme" },
"page_token": "k3f…q9.ab12cd",
"fields": { "message": "Let's talk" },
"created_at": "2026-06-04T19:02:55Z"
}import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody MUST be the exact bytes received (do not re-serialize parsed JSON).
export function verifyCrutanSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("="))
);
const expected = createHmac("sha256", secret)
.update(parts.t + "." + rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}2xx within a few seconds to acknowledge. Non-2xx responses are retried with exponential backoff; persistent failures are recorded and surfaced in the connectors dashboard.MCP server
The Crutan MCP server exposes 40+ tools and 7 skill prompts so agents (Claude, Cursor, etc.) can run full workflows — campaigns, discovery pages, CRM hooks, and reporting. It authenticates with your workspace API key; plan limits, credit balances, and monthly key spend caps apply.
Transports
- stdio — local agents via
pnpm --filter @crutan/mcp start - HTTP —
https://app.crutan.com/api/mcpwithAuthorization: Bearer crt_…(per-user key, not a shared server secret)
Skill prompts
Invoke by name to receive a step-by-step playbook: launch-campaign, discovery-workflow, onboard-workspace, weekly-conversion-report, connect-crm, research-and-personalize, reactivate-cold-leads. Catalog resource: crutan://skills/catalog.
Cost guardrails
Call estimate_cost before spend tools; pass the exact acknowledgedEstimateCents or acknowledgedDiscoveryCredits returned. Async tools (create_template, build_discovery_page) return immediately — poll until complete.
OpenAPI
The full machine-readable specification is published as OpenAPI 3.1. Import it into Postman, generate a client, or wire it into an iPaaS tool.