curl --fail-with-body --max-time 30 -X POST "https://api.freshtalent.ai/v1/targets" \
-H "Authorization: Bearer $FRESHTALENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"anduril.com","kind":"company"}'
curl --fail-with-body --max-time 30 -X POST "https://api.freshtalent.ai/v1/targets" \
-H "Authorization: Bearer $FRESHTALENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input":"Foundry FDE discovery",
"kind":"people",
"app_query": {
"keywords":"Foundry",
"roles":[{"family":"forward-deployed-engineer"}]
},
"result_limit": 80
}'
const response = await fetch("https://api.freshtalent.ai/v1/targets", {
method: "POST",
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${process.env.FRESHTALENT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: "Foundry FDE discovery",
kind: "people",
app_query: { keywords: "Foundry", roles: [{ family: "forward-deployed-engineer" }] },
}),
});
const body = await response.json();
if (![200, 202].includes(response.status)) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body.data.id, body.data.job.status);
import json
import os
import urllib.request
payload = json.dumps({
"input": "Foundry FDE discovery",
"kind": "people",
"app_query": {"keywords": "Foundry", "roles": [{"family": "forward-deployed-engineer"}]},
}).encode()
request = urllib.request.Request(
"https://api.freshtalent.ai/v1/targets",
data=payload,
method="POST",
headers={
"Authorization": f"Bearer {os.environ['FRESHTALENT_API_KEY']}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(request, timeout=30) as response:
body = json.load(response)
print(body["data"]["id"], body["data"]["job"]["status"])
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"kind": "people",
"name": "Foundry FDE discovery",
"linkedin_url": null,
"auto_admit": false,
"watching_count": 0,
"proposed_count": 0,
"excluded_count": 0,
"current_count": 0,
"alum_count": 0,
"enriched_count": 0,
"member_count": 0,
"allocation_used": 0,
"in_graph_count": 0,
"result_limit": 80,
"created_at": "2026-09-16T11:43:00.000Z",
"query": {
"input": "Foundry FDE discovery",
"filters": { "keywords": "Foundry", "keyword_leaves": ["Foundry"], "titles": ["Forward Deployed Engineer", "Forward-Deployed Engineer", "FDE"] },
"app": { "keywords": "Foundry", "roles": [{ "family": "forward-deployed-engineer" }] },
"result_limit": 80
},
"job": {
"id": "7b1d9b3c-0b8f-4d4d-a8b4-72ef8fbb4c81",
"status": "pending",
"phase": "queued",
"total": 0,
"completed": 0,
"failed": 0,
"error": null
}
}
}
Create target
Create a people-search or company-network Target and enqueue discovery.
POST
/
targets
curl --fail-with-body --max-time 30 -X POST "https://api.freshtalent.ai/v1/targets" \
-H "Authorization: Bearer $FRESHTALENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"anduril.com","kind":"company"}'
curl --fail-with-body --max-time 30 -X POST "https://api.freshtalent.ai/v1/targets" \
-H "Authorization: Bearer $FRESHTALENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input":"Foundry FDE discovery",
"kind":"people",
"app_query": {
"keywords":"Foundry",
"roles":[{"family":"forward-deployed-engineer"}]
},
"result_limit": 80
}'
const response = await fetch("https://api.freshtalent.ai/v1/targets", {
method: "POST",
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${process.env.FRESHTALENT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: "Foundry FDE discovery",
kind: "people",
app_query: { keywords: "Foundry", roles: [{ family: "forward-deployed-engineer" }] },
}),
});
const body = await response.json();
if (![200, 202].includes(response.status)) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body.data.id, body.data.job.status);
import json
import os
import urllib.request
payload = json.dumps({
"input": "Foundry FDE discovery",
"kind": "people",
"app_query": {"keywords": "Foundry", "roles": [{"family": "forward-deployed-engineer"}]},
}).encode()
request = urllib.request.Request(
"https://api.freshtalent.ai/v1/targets",
data=payload,
method="POST",
headers={
"Authorization": f"Bearer {os.environ['FRESHTALENT_API_KEY']}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(request, timeout=30) as response:
body = json.load(response)
print(body["data"]["id"], body["data"]["job"]["status"])
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"kind": "people",
"name": "Foundry FDE discovery",
"linkedin_url": null,
"auto_admit": false,
"watching_count": 0,
"proposed_count": 0,
"excluded_count": 0,
"current_count": 0,
"alum_count": 0,
"enriched_count": 0,
"member_count": 0,
"allocation_used": 0,
"in_graph_count": 0,
"result_limit": 80,
"created_at": "2026-09-16T11:43:00.000Z",
"query": {
"input": "Foundry FDE discovery",
"filters": { "keywords": "Foundry", "keyword_leaves": ["Foundry"], "titles": ["Forward Deployed Engineer", "Forward-Deployed Engineer", "FDE"] },
"app": { "keywords": "Foundry", "roles": [{ "family": "forward-deployed-engineer" }] },
"result_limit": 80
},
"job": {
"id": "7b1d9b3c-0b8f-4d4d-a8b4-72ef8fbb4c81",
"status": "pending",
"phase": "queued",
"total": 0,
"completed": 0,
"failed": 0,
"error": null
}
}
}
Creates a standing Target and, when needed, a background discovery job. The operation is asynchronous: creation returns the Target and its current job summary, not the discovered member set.
A company Target tracks the current company network, including current and alumni discovery performed by the worker.
A people Target runs the stored people query. The service always creates a new people Target and enqueues a
All properties are optional. Arrays may be empty; the route declares no per-array item limit. Unknown properties are stripped by the framework. These values are passed into the people discovery query.
Every top-level field is optional; each
For exact repeat searches, save and replay the query through Search people. For discovery, build the documented Target inputs deliberately and review the returned
Request body
The body is a JSON object. Only documented fields are supported; the framework strips unknown top-level fields.input is required; the other fields are optional.
string
required
1–400 characters before trimming. For
company, use a company name, domain, or LinkedIn company URL. For people, use the Target name or fallback search text. Whitespace-only input passes the JSON length check but is rejected after trimming.string
Exact enum:
company or people. Omitted means company in the service. Do not rely on inference: send the discriminator explicitly.boolean
Default
false. With false, discovered members start as proposed; with true, they start as watching. watching members are eligible for enrichment and Monitoring. Changing this later also promotes existing proposed members; it does not promote excluded members.integer
Optional. How many people this Target should discover. Minimum
1. The service clamps the value to the deployment cap (TRACK_COMPANY_MAX_PEOPLE, default 4000). Omit it to use that deployment cap. The dashboard always sends a limit, using remaining monitoring capacity as the ceiling.kind: "company"
A company Target tracks the current company network, including current and alumni discovery performed by the worker. input is provisionally normalized locally, then resolved asynchronously. The company Target is unique per organization and resolved company. If an existing company Target has a latest job in pending, running, or done, this request returns it with 200 and does not enqueue another job. A failed latest job can be re-enqueued.
query and app_query are accepted by the route schema but are not used by the company creation service. Omit them.
kind: "people"
A people Target runs the stored people query. The service always creates a new people Target and enqueues a track_people job; there is no request idempotency key.
object
Optional provider-oriented discovery filters for
kind: "people". Supported properties and types are listed below. This is not the echoed graph-search query.query is the direct Gateway query. Its exact accepted shape is:
{
"keywords": "string",
"titles": ["string"],
"past_titles": ["string"],
"seniorities": ["string"],
"location_ids": ["string"],
"exclude_location_ids": ["string"],
"recently_changed_jobs": true,
"company_ids": ["string"],
"past_company_ids": ["string"]
}
query.keywords, when present, overrides the generated fallback keyword from input/app_query.
object
Optional camelCase app-query object for
kind: "people". Use role-object and string arrays as described below; do not paste the snake_case graph-search echo unchanged.app_query is an object with the following UI query shape. The route validates only that it is an object, so nested types are a service contract rather than a framework validation guarantee:
{
"keywords": "string",
"company": "string",
"location": "string",
"excludeLocations": "string",
"roles": [{
"family": "string",
"minMonths": 12,
"scope": "string",
"title": "string",
"titles": ["string"],
"titleMatch": "exact"
}],
"companyCurrent": true,
"companyLeftMinMonthsAgo": 18,
"betweenRoles": false,
"skill": "string",
"skills": ["string"],
"skillMatch": "any",
"language": "string",
"languages": ["string"],
"languageMatch": "all",
"excludeLanguages": ["string"],
"school": "string"
}
roles item has required family: string, with the other role fields optional. The route declares no nested array length limits. Verified enums are roles[].titleMatch: exact | contains, skillMatch: any | all, and languageMatch: any | all. The mapper uses keywords/school, skills, languages, role titles/families, location/exclusions, and maps a positive companyLeftMinMonthsAgo or betweenRoles: true to recently_changed_jobs: true in the stored Gateway query; the numeric month value itself is not retained there. company may contribute to the UI-derived Target name, but neither company nor companyCurrent is copied into PeopleQuery.company_ids; use verified provider query.company_ids when a company filter is required; the echoed graph-search object is not a substitute. minMonths and scope are accepted role fields but are not mapped into the stored PeopleQuery by this service.
Certification is supported by graph people search, but is not supported by people Target discovery. Supplying either app_query.certification or app_query.certifications returns 400 certification_target_unsupported; save and replay the echoed query through Search people when certification matching is required.
Do not pass the echoed snake_case people-search
query straight into app_query. The two objects have different types and different meanings. Some fields would be ignored; a string roles value can cause a service error because the mapper expects an array. Do not invent provider company or location IDs.Search query compatibility
There is no lossless REST conversion from a graph search to a discovery Target:| Graph search | Target discovery |
|---|---|
roles is a token string | app_query.roles is an array of role objects |
skills / languages are delimited strings | app_query.skills / languages are string arrays |
role_family and snake_case availability flags | Not read by the Target app-query mapper |
company matches graph company text | app_query.company does not set a provider company filter |
company_left_min_months_ago: 18 means left at least 18 months ago | app_query.companyLeftMinMonthsAgo > 0 maps only to a provider recently-changed-jobs flag; it does not preserve the 18-month condition |
certification / certifications match recorded graph credentials | People Target discovery rejects these certification filters; they are not provider filters |
| Role tenure and scope | Not preserved in discovery filters |
data.query.filters and members. The MCP has an adapter for echoed queries, but that adapter does not make unsupported provider filters equivalent to graph filters.
Response
202 Acceptedmeans a background discovery job was enqueued. A company Target may have been reused rather than newly created.200 OKmeans the request reused an existing company Target because its latest job waspending,running, ordone.
{ "data": Target } with the same Target schema as List targets. job.status is normally pending immediately after enqueue. job.phase starts as queued; later phases may include resolving, discovering, enriching, and done. job.id and job.error are nullable. linkedin_url is nullable until company resolution supplies one and is always null for people Targets.
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"kind": "people",
"name": "Foundry FDE discovery",
"linkedin_url": null,
"auto_admit": false,
"watching_count": 0,
"proposed_count": 0,
"excluded_count": 0,
"current_count": 0,
"alum_count": 0,
"enriched_count": 0,
"member_count": 0,
"allocation_used": 0,
"in_graph_count": 0,
"result_limit": 80,
"created_at": "2026-09-16T11:43:00.000Z",
"query": {
"input": "Foundry FDE discovery",
"filters": { "keywords": "Foundry", "keyword_leaves": ["Foundry"], "titles": ["Forward Deployed Engineer", "Forward-Deployed Engineer", "FDE"] },
"app": { "keywords": "Foundry", "roles": [{ "family": "forward-deployed-engineer" }] },
"result_limit": 80
},
"job": {
"id": "7b1d9b3c-0b8f-4d4d-a8b4-72ef8fbb4c81",
"status": "pending",
"phase": "queued",
"total": 0,
"completed": 0,
"failed": 0,
"error": null
}
}
}
Examples
curl --fail-with-body --max-time 30 -X POST "https://api.freshtalent.ai/v1/targets" \
-H "Authorization: Bearer $FRESHTALENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"anduril.com","kind":"company"}'
curl --fail-with-body --max-time 30 -X POST "https://api.freshtalent.ai/v1/targets" \
-H "Authorization: Bearer $FRESHTALENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input":"Foundry FDE discovery",
"kind":"people",
"app_query": {
"keywords":"Foundry",
"roles":[{"family":"forward-deployed-engineer"}]
},
"result_limit": 80
}'
const response = await fetch("https://api.freshtalent.ai/v1/targets", {
method: "POST",
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${process.env.FRESHTALENT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: "Foundry FDE discovery",
kind: "people",
app_query: { keywords: "Foundry", roles: [{ family: "forward-deployed-engineer" }] },
}),
});
const body = await response.json();
if (![200, 202].includes(response.status)) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body.data.id, body.data.job.status);
import json
import os
import urllib.request
payload = json.dumps({
"input": "Foundry FDE discovery",
"kind": "people",
"app_query": {"keywords": "Foundry", "roles": [{"family": "forward-deployed-engineer"}]},
}).encode()
request = urllib.request.Request(
"https://api.freshtalent.ai/v1/targets",
data=payload,
method="POST",
headers={
"Authorization": f"Bearer {os.environ['FRESHTALENT_API_KEY']}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(request, timeout=30) as response:
body = json.load(response)
print(body["data"]["id"], body["data"]["job"]["status"])
Lifecycle, side effects, and retries
Creation inserts a Target, enqueues discovery, and attempts to create its default list. Discovery and enrichment happen later. Discovery is capped byresult_limit when provided, otherwise by TRACK_COMPANY_MAX_PEOPLE (default 4000, configurable at deployment); each saturated provider search leaf is capped at 2500. auto_admit: false leaves new members proposed; setting members to watching is the operation that queues enrichment. A run selects up to 2000 initially unenriched watching members, or up to 500 stale members during refresh, so large Targets may require later work. The worker deduplicates discovered URLs within a Target and uses bounded enrichment retries internally. Discovery usually finishes within 24 hours; the API does not promise a completion time or monitoring freshness.
Do not blindly retry a timed-out POST: a people request may already have created a second Target, while a company request may have reused or created a job. First call List targets and reconcile by id, kind, name, and stored query. A 202 is acceptance of the job, not completion.
Errors and recovery
400 invalid_request: missing/blankinput, invalidkind, invalid top-level type, or invalid JSON schema. Fix the body.401 unauthorized,402 payment_required: fix the API key or organization access.404 not_found: can surface from a service lookup; verify the organization and input.409 conflict: the route preserves a service conflict response if one occurs; reconcile before replaying.429/5xx: back off. For an uncertain write, read the Target collection before retrying rather than replaying blindly.- A
202Target whosejob.statuslater becomesfailedis not fixed by polling faster; inspectjob.error, then use Retry target after the underlying issue is resolved.
Next
- Get a target or List targets to reconcile asynchronous progress.
- List target members to inspect
proposedresults. - Accept target members to move selected members to
watching.