Skip to main content

API Documentation

Integrate WonderFunnel into your recruitment stack

Getting Started

Base URL

https://app.wonderfunnel.io

The WonderFunnel API lets you programmatically upload candidate profiles, trigger ICP analysis, and retrieve intelligence reports. All requests use HTTPS and return JSON.

Quick Start

  1. Generate an API key in Settings → Integrations.
  2. Include the key as a Bearer token in every request header.
  3. Upload 5 candidate profiles for a target role.
  4. 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_here

Keep 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

StatusMeaning
401Missing or invalid API key
403Authenticated but not authorised for this resource
404Resource not found
429Rate limit exceeded — see Rate Limits section
500Server 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.

POST/api/upload-files

Upload one or more candidate profile PDFs for a role.

Request — multipart/form-data

FieldTypeDescription
filesFile[]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.

POST/api/generate-icp

Start ICP analysis for uploaded profiles. Returns immediately; processing takes 30–90 seconds.

Request body — application/json

FieldTypeRequiredDescription
client_idstringYesYour client UUID from the upload response
function_groupstringYesRole or function group name, e.g. "Senior Data Engineer"
placement_idsstring[]YesArray 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"
}
GET/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.

GET/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

EventTrigger
icp.readyICP analysis completed successfully
icp.failedICP analysis failed
campaign.results_readyMicro-test results available
risk.high_scoreA 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 GroupLimitWindow
/api/upload-files20 requests1 minute
/api/generate-icp10 requests1 minute
/api/icp/*60 requests1 minute
All other /api/*100 requests1 minute

When you hit a limit, the API returns 429 with a Retry-After header indicating seconds to wait.