Skip to main content
POST
/
api
/
v1
/
research
Submit a person for deep research (partner API)
curl --request POST \
  --url https://app.puffle.ai/api/v1/research \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "first_name": "<string>",
  "last_name": "<string>",
  "linkedin_url": "<string>"
}
'
{
  "person": {
    "id": "11111111-1111-1111-1111-111111111111",
    "user_id": null,
    "first_name": "Jane",
    "last_name": "Smith",
    "linkedin_url": "https://www.linkedin.com/in/janesmith",
    "role": "VP of Engineering",
    "company": "Acme Corp",
    "company_domain": "acme.com",
    "photo_url": null,
    "status": "complete",
    "last_enriched_at": "2026-04-21T18:00:00Z",
    "created_at": "2026-04-21T17:55:00Z",
    "updated_at": "2026-04-21T18:00:00Z"
  },
  "status": "complete",
  "report": {
    "summary": "Jane Smith is VP of Engineering at Acme Corp...",
    "icp_score": 91
  },
  "change_summary": null
}

Overview

The public partner entry point for deep research. Accepts a person identified by first_name, last_name, and linkedin_url, and returns a partner-scoped envelope containing the person row, current status, and (when ready) the report content inline. The LinkedIn URL is normalized server-side to https://www.linkedin.com/in/<slug> and used as the dedup key per partner. Resubmitting the same URL does NOT create a duplicate — instead, the server branches on the existing row’s current status:
Existing statusBehaviorResponse code
completeReturn the cached report synchronously. No pipeline re-run.200
running / idleReturn current status with report: null. No duplicate pipeline launch.200
failedAtomically flip to running, re-trigger the pipeline.202
no existing rowInsert new row, trigger the pipeline.202
Pipeline runs on Trigger.dev (partner-deep-research-enrich) and typically completes in 1 to 5 minutes.

AI agent notes

Async via Trigger.dev. A 202 response means the pipeline is queued, not that the report is ready. A 200 response with status: "complete" means the report is already cached and returned inline.Polling sequence:
  1. Call createResearchV1 with the target person.
  2. If the response is 200 with status: "complete", you have the report — done.
  3. If the response is 202 or 200 with status: "running", poll getResearchV1 with the returned person.id every 30 seconds.
  4. Stop when status is complete (report inline) or failed (retry by re-submitting the same payload to createResearchV1).
Idempotency. Safe to re-submit the same linkedin_url repeatedly — duplicate submissions return the cached row rather than queueing new work. Use this as the canonical retry primitive.Input constraints:
  • All three body fields are required — missing any returns 400.
  • first_name and last_name are capped at 100 characters each.
  • linkedin_url must contain linkedin.com/in/ somewhere in the string.
Authentication. Partner API key, passed as a Bearer token. User-account Bearer tokens are rejected with 401.

Authorizations

Authorization
string
header
required

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Body

application/json

All three fields are required. The server uses the normalized LinkedIn URL as the dedup key per partner — re-submitting the same URL returns the existing row rather than creating a duplicate.

first_name
string
required

Person's first name. Required. Max 100 characters.

Maximum string length: 100
last_name
string
required

Person's last name. Required. Max 100 characters.

Maximum string length: 100
linkedin_url
string
required

LinkedIn profile URL. Required. Must contain linkedin.com/in/. Normalized server-side to https://www.linkedin.com/in/<slug>.

Response

Returned when the person already exists for this partner — either complete (report inline) or running/idle (report null). Idempotent; no pipeline side effect.

The canonical partner-API envelope for a single research person. Same shape is returned by createResearchV1, getResearchV1, and the duplicate-return path of createResearchV1.

person
object
required

A person tracked for deep research. The row is created first; enrichment + report generation happen asynchronously in a Trigger.dev pipeline.

status
enum<string>
required

Lifecycle of a deep research person. idle is a fresh row with no run in flight; running means a Trigger.dev pipeline is active; complete means the latest report is ready; failed means the pipeline errored and the row is retry-eligible.

Available options:
idle,
running,
complete,
failed
report
object
required

Report body — a JSON object with summary, background, company intel, outreach angles, and source citations. Shape evolves; treat fields defensively.

change_summary
string | null
required

Human-readable diff vs the prior version. Null on first version or when no report exists.