← All docsFix & share

The HTTP API

Drive Harpoon from your own code: mint a token, upload a capture, poll the job, read the findings — plus the published OpenAPI spec.

What the API is for#

Everything the web app does to a capture, it does through this API — and an API token lets your own scripts, CI jobs and agents do the same: upload a capture, analyze it, diff two runs, and read the results back. The engine is deterministic, so the same capture always produces the same score and the same finding ids; the AI report only explains findings the engine already found.

If you just want performance checks in CI, use the CLI instead — it wraps these calls, runs the engine locally, and needs no token unless you want the runs saved. Come here when you are building something the CLI does not do.

The machine-readable contract is published at https://api.harpoon.solutions/openapi.json — OpenAPI 3.1, describing exactly the operations a token may call and nothing else. Point a code generator or an agent at it.

Authentication#

Mint a token under Settings → CI & API tokens (owners and admins only). The secret is shown exactly once, at creation — store it immediately; there is no way to read it back. Send it as a bearer header on every request:

export HARPOON_TOKEN="hpn_..."   # from Settings → CI & API tokens

curl -H "Authorization: Bearer $HARPOON_TOKEN" \
     https://api.harpoon.solutions/api/analyses
  • A token acts as its workspace, with member-level rights. It reads and writes that workspace's data and nothing else — a token from one workspace cannot see another's, and gets the same plan limits the workspace has.
  • The reachable surface is a deliberate allowlist: upload, analyze, compare, poll a job, read reports/findings/comparisons, and list or create projects. Everything else answers 403 TOKEN_FORBIDDEN.
  • That includes token management itself — a token cannot mint or revoke tokens, change billing, or alter workspace membership. Losing a token cannot cost you the workspace.
  • Revoke a token from the same settings page. Revocation is immediate: the next request with it gets 401.

The round trip: upload → analyze → poll → read#

Analysis is asynchronous by default. POST /api/analyze answers 202 with a jobId; you poll the job, then read the finished analysis. Here is the whole loop, in order:

# 1. Send the capture and start the analysis (one call — no separate upload needed)
curl -sS -X POST https://api.harpoon.solutions/api/analyze \
  -H "Authorization: Bearer $HARPOON_TOKEN" \
  -F "file=@page.har"
# → 202 {"jobId":"analyze:21be5b95-…:none","uploadId":"21be5b95-…"}

# 2. Poll until status is completed (or failed). Treat the job id as an opaque
#    string — it is not a number, and its shape is not part of the contract.
curl -sS -H "Authorization: Bearer $HARPOON_TOKEN" \
  "https://api.harpoon.solutions/api/jobs/analyze:21be5b95-…:none"
# → {"jobId":"analyze:21be5b95-…:none","status":"active","analysisId":"19fa35b4-…",
#    "error":null,"progress":{"stage":"report","pct":70,"ceiling":95,"requests":148}}

# 3. Read the finished analysis — findings, score, timeline, AI report
curl -sS -H "Authorization: Bearer $HARPOON_TOKEN" \
  "https://api.harpoon.solutions/api/report/19fa35b4-…"
analysisId arrives before the job finishes. The analysis row is persisted before the AI report is written, so the job reports its id while status is still active. You can start reading findings then — just pass ?generate=0 (below) so the read does not kick off a second report.

Prefer one blocking call? Add ?sync=1 and the analysis runs inline, returning the full 201 result instead of a job id. Simpler to script, but the request is held open for the whole analysis — including the model round trip — so it is a poor fit for anything with a timeout.

curl -sS -X POST "https://api.harpoon.solutions/api/analyze?sync=1" \
  -H "Authorization: Bearer $HARPOON_TOKEN" \
  -F "file=@page.har"
# → 201 {"analysisId":"9a7b…","analysis":{…},"report":{…},"permalink":"https://…"}

Reading results without paying for a report#

GET /api/report/:id generates the AI report if the analysis does not have one yet — a model round trip that can take tens of seconds. When you only want the deterministic findings, or you are polling, say so:

# The findings and score, never a model call. report is null if none is stored.
curl -sS -H "Authorization: Bearer $HARPOON_TOKEN" \
  "https://api.harpoon.solutions/api/report/9a7b…?generate=0"

# Just the findings — a much smaller response than the full analysis
curl -sS -H "Authorization: Bearer $HARPOON_TOKEN" \
  "https://api.harpoon.solutions/api/findings/9a7b…?severity=critical"

GET /api/analyses lists the workspace's runs newest-first with limit and offset. Each row carries the score, grade, request count, transferred bytes and the severity histogram — enough to build a dashboard without fetching a single full analysis.

Comparing two captures#

POST /api/compare takes two captures — before and after — and returns the diff plus its report. It runs inline (there is no job for a comparison) and persists both analyses and the comparison, so you can read it back later with GET /api/comparison/:id.

curl -sS -X POST https://api.harpoon.solutions/api/compare \
  -H "Authorization: Bearer $HARPOON_TOKEN" \
  -F "before=@baseline.har" \
  -F "after=@candidate.har"
# → 201 {"comparisonId":"c41e…","result":{…},"report":{…}}

Already uploaded both? Send { "beforeUploadId": "…", "afterUploadId": "…" } as JSON instead. See Comparing runs for how to read a verdict.

Grouping runs into a project timeline#

Pass projectId when you analyze and the run joins that project's timeline, which is what powers trends and regression alerts (Projects & trends).

# Create a project once
curl -sS -X POST https://api.harpoon.solutions/api/projects \
  -H "Authorization: Bearer $HARPOON_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"marketing-site","alertScoreDrop":5}'
# → 201 {"project":{"id":"b48485cc-…","name":"marketing-site",…}}

# Then attach every run to it
curl -sS -X POST https://api.harpoon.solutions/api/analyze \
  -H "Authorization: Bearer $HARPOON_TOKEN" \
  -F "file=@page.har" -F "projectId=b48485cc-…"

# And read the timeline
curl -sS -H "Authorization: Bearer $HARPOON_TOKEN" \
  "https://api.harpoon.solutions/api/projects/b48485cc-…/trends?limit=20"

A project can hold runs for several pages; trends default to the most recently active one, and the response's pages array lists the others so you can ask for a specific pageKey.

The complete surface#

These are all the operations a token can reach. Anything not in this table answers 403 TOKEN_FORBIDDEN — and a test in the repo fails the build if this list and the published spec ever disagree.

OperationWhat it does
POST /api/uploadStore a capture without analyzing it yet.
POST /api/analyzeAnalyze a capture. 202 + job id, or 201 with ?sync=1.
POST /api/compareDiff two captures. Runs inline.
GET /api/jobs/:idPoll an analysis job.
GET /api/report/:idThe full stored analysis + report. ?generate=0 to skip the AI.
GET /api/findings/:idJust the findings. ?severity= and ?category= filter.
GET /api/comparison/:idA stored comparison.
GET /api/analysesThe workspace's runs, newest first. ?limit= and ?offset=.
GET /api/projectsList projects.
POST /api/projectsCreate a project (optionally with alert thresholds).
GET /api/projects/:id/trendsA project timeline. ?pageKey= and ?limit=.

Errors, limits and retries#

Every non-2xx response uses one envelope, so a client only needs one error path: { "error": { "code": "NOT_FOUND", "message": "…" } }. The code is stable and machine-readable; the message is safe to show a person.

StatusWhen, and what to do
400Malformed request — the code names the field. Fix and resend; retrying unchanged will not help.
401Unknown or revoked token. Mint a new one.
403Outside the token allowlist, or your plan does not include the feature.
404No such record in this workspace. Another workspace's record looks identical to a missing one.
402A plan limit was reached — the code is PLAN_LIMIT.
413The capture is over this deployment’s upload ceiling. Read the limit from the message rather than hard-coding one.
429Rate limited. Back off and retry.
503Only from GET /api/jobs/:id, on a deployment that runs analyses inline and has no queue. Use ?sync=1.

Rate limits are per workspace and apply to token and browser traffic alike, with a tighter limit on the analyze and compare routes because each one runs a real analysis. Treat 429 as "slow down", not "stop": retry with a growing delay.

What happens to your capture#

The raw capture is deleted as soon as it has been analyzed — usually within seconds. What persists is what we derived: the findings, score, and request timeline. The AI report is written from that curated projection only; your raw requests, cookies, headers and response bodies never reach a model. Security & data has the full detail.

Still stuck? Contact us or browse all guides.