API Documentation
Integrate WonderFunnel into your recruitment stack
Getting Started
Base URL
https://app.wonderfunnel.ioThe WonderFunnel API lets you programmatically upload candidate profiles, trigger ICP analysis, and retrieve intelligence reports. All requests use HTTPS and return JSON.
Quick Start
- Generate an API key in Settings → Integrations.
- Include the key as a Bearer token in every request header.
- Upload 5 candidate profiles for a target role.
- Trigger ICP generation. Poll for status, then retrieve the report.
# Example: check your authentication
curl -s https://app.wonderfunnel.io/api/health \
-H "Authorization: Bearer wf_live_your_key_here"Authentication
All API requests must include your API key in the Authorization header. Keys are prefixed with wf_live_.
Authorization: Bearer wf_live_your_api_key_hereKeep your key secret. Do not expose it in client-side code or public repositories. If a key is compromised, revoke it immediately in Settings → Integrations.
Error responses
| Status | Meaning |
|---|---|
401 | Missing or invalid API key |
403 | Authenticated but not authorised for this resource |
404 | Resource not found |
429 | Rate limit exceeded — see Rate Limits section |
500 | Server error — safe to retry with backoff |
SDKs & Code Examples
Use the language of your choice. The examples below use the OpenAPI spec to generate typed clients, or copy-paste the snippets directly.
Node.js / TypeScript
import fetch from "node-fetch"; // or use native fetch in Node 18+
const BASE = "https://app.wonderfunnel.io/api/v1";
const KEY = process.env.WONDERFUNNEL_API_KEY!; // wf_live_...
async function submitCandidate(
functionGroup: string,
candidateText: string,
source = "ats"
) {
const res = await fetch(`${BASE}/placements`, {
method: "POST",
headers: {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ function_group: functionGroup, candidate_text: candidateText, source }),
});
if (!res.ok) throw new Error(`WonderFunnel API error: ${res.status}`);
return (await res.json() as { data: { id: string } }).data.id;
}
async function listIcps(functionGroup?: string) {
const params = new URLSearchParams({ limit: "50" });
if (functionGroup) params.set("function_group", functionGroup);
const res = await fetch(`${BASE}/icps?${params}`, {
headers: { "Authorization": `Bearer ${KEY}` },
});
if (!res.ok) throw new Error(`WonderFunnel API error: ${res.status}`);
return (await res.json() as { data: unknown[] }).data;
}Python
import os, requests
BASE = "https://app.wonderfunnel.io/api/v1"
KEY = os.environ["WONDERFUNNEL_API_KEY"] # wf_live_...
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {KEY}"})
def submit_candidate(function_group: str, candidate_text: str, source: str = "ats") -> str:
"""Returns the new placement ID."""
r = session.post(f"{BASE}/placements", json={
"function_group": function_group,
"candidate_text": candidate_text,
"source": source,
})
r.raise_for_status()
return r.json()["data"]["id"]
def list_icps(function_group: str | None = None) -> list[dict]:
params = {"limit": 50}
if function_group:
params["function_group"] = function_group
r = session.get(f"{BASE}/icps", params=params)
r.raise_for_status()
return r.json()["data"]
def register_webhook(url: str, events: list[str]) -> dict:
r = session.post(f"{BASE}/webhooks", json={"url": url, "events": events})
r.raise_for_status()
result = r.json()
print(f"Signing secret (save this): {result['signing_secret']}")
return result["data"]Go
package wonderfunnel
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const baseURL = "https://app.wonderfunnel.io/api/v1"
type Client struct{ apiKey string }
func New() *Client { return &Client{apiKey: os.Getenv("WONDERFUNNEL_API_KEY")} }
func (c *Client) do(method, path string, body any) (*http.Response, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, baseURL+path, &buf)
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
return http.DefaultClient.Do(req)
}
func (c *Client) SubmitCandidate(functionGroup, text, source string) (string, error) {
resp, err := c.do("POST", "/placements", map[string]string{
"function_group": functionGroup, "candidate_text": text, "source": source,
})
if err != nil { return "", err }
defer resp.Body.Close()
if resp.StatusCode != 201 { return "", fmt.Errorf("API error: %d", resp.StatusCode) }
var out struct{ Data struct{ ID string `json:"id"` } `json:"data"` }
json.NewDecoder(resp.Body).Decode(&out)
return out.Data.ID, nil
}Upload Profiles
Upload PDF candidate profiles for a role. Each upload creates a placement record that serves as input for ICP generation. You need at least 3 profiles; 5 gives the best results.
/api/upload-filesUpload one or more candidate profile PDFs for a role.
Request — multipart/form-data
| Field | Type | Description |
|---|---|---|
| files | File[] | PDF files, max 10 MB each, 5 files max |
Example
curl -s -X POST https://app.wonderfunnel.io/api/upload-files \
-H "Authorization: Bearer wf_live_your_key_here" \
-F "files=@candidate1.pdf" \
-F "files=@candidate2.pdf" \
-F "files=@candidate3.pdf"Response
{
"success": true,
"placement_ids": ["uuid-1", "uuid-2", "uuid-3"],
"client_id": "your-client-uuid"
}Generate ICP
Trigger ICP generation for a set of uploaded profiles. Analysis runs asynchronously — poll /api/icp-status to check progress.
/api/generate-icpStart ICP analysis for uploaded profiles. Returns immediately; processing takes 30–90 seconds.
Request body — application/json
| Field | Type | Required | Description |
|---|---|---|---|
| client_id | string | Yes | Your client UUID from the upload response |
| function_group | string | Yes | Role or function group name, e.g. "Senior Data Engineer" |
| placement_ids | string[] | Yes | Array of placement IDs from the upload response |
Example
curl -s -X POST https://app.wonderfunnel.io/api/generate-icp \
-H "Authorization: Bearer wf_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"client_id": "your-client-uuid",
"function_group": "Senior Data Engineer",
"placement_ids": ["uuid-1", "uuid-2", "uuid-3"]
}'Response
{
"success": true,
"icp_profile_id": "icp-uuid",
"status": "processing"
}/api/icp-status?client_id={client_id}Poll ICP processing status. Returns the latest ICP profile status for the given client.
curl -s "https://app.wonderfunnel.io/api/icp-status?client_id=your-client-uuid" \
-H "Authorization: Bearer wf_live_your_key_here"Response
{
"status": "analyzed",
"icp_profile_id": "icp-uuid",
"function_group": "Senior Data Engineer"
}Status values: processing, analyzed, failed
Retrieve Reports
Once an ICP is analyzed, retrieve the full intelligence report including segments, 6W factors, risk score, and messaging hypotheses.
/api/icp/{icp_profile_id}Retrieve a complete ICP intelligence report by ID.
curl -s "https://app.wonderfunnel.io/api/icp/icp-profile-uuid" \
-H "Authorization: Bearer wf_live_your_key_here"Response (abbreviated)
{
"id": "icp-profile-uuid",
"function_group": "Senior Data Engineer",
"isco_code": "2521",
"executive_summary": "...",
"segments": [
{
"label": "Scale Seekers",
"description": "...",
"six_w_factors": [
{
"dimension": "WHY",
"factor_short": "Outgrown current data maturity",
"factor_statement": "I've maxed out what I can learn here — the infrastructure is too immature.",
"resonance_level": "high"
}
]
}
],
"risk_score": {
"total_risk_score": 7.4,
"hiring_difficulty_score": 8.1,
"hiring_need_score": 6.8,
"strategic_relevance_score": 7.0
},
"messaging_hypotheses": [
{
"headline": "Scale your data infrastructure",
"angle": "WHY",
"predicted_resonance": "high"
}
]
}Webhooks
WonderFunnel can push events to your endpoint when key actions complete. Configure your webhook URL in Settings → Integrations.
Events
| Event | Trigger |
|---|---|
icp.ready | ICP analysis completed successfully |
icp.failed | ICP analysis failed |
campaign.results_ready | Micro-test results available |
risk.high_score | A role's risk score exceeds 7.0 |
Payload format
{
"event": "icp.ready",
"timestamp": "2026-04-07T10:30:00Z",
"client_id": "your-client-uuid",
"data": {
"icp_profile_id": "icp-profile-uuid",
"function_group": "Senior Data Engineer",
"total_risk_score": 7.4
}
}Verifying webhook signatures
Each webhook includes a X-WonderFunnel-Signature header — an HMAC-SHA256 of the raw request body, signed with your webhook secret.
import { createHmac } from "crypto";
function verifySignature(body, signature, secret) {
const expected = createHmac("sha256", secret)
.update(body)
.digest("hex");
return signature === expected;
}Rate Limits
API requests are rate-limited per API key. Limits apply per endpoint group.
| Endpoint Group | Limit | Window |
|---|---|---|
/api/upload-files | 20 requests | 1 minute |
/api/generate-icp | 10 requests | 1 minute |
/api/icp/* | 60 requests | 1 minute |
All other /api/* | 100 requests | 1 minute |
When you hit a limit, the API returns 429 with a Retry-After header indicating seconds to wait.