REST API

Read your sites, findings and friction data over HTTP. Every response is JSON. Everything here is read-only except the outcome and registration endpoints, which are explicitly marked.

Authentication

Send an API key as a bearer token on every request:

Authorization: Bearer hv_live_xxxxxxxxxxxx

Create one in the dashboard under Settings, then API keys. A key is scoped to your organisation, so it reaches every site your org owns, and it is shown once at creation.

Rate limits

RequestsLimit
Authenticated (any request carrying a bearer token)300 per minute per IP
Unauthenticated60 per minute per IP

Over the limit returns 429. In practice every call on this page is authenticated, so 300/min is the number that applies to you.

List sites

GET /v1/sites

Every site your organisation owns.

curl -H "Authorization: Bearer hv_live_xxx" \
  https://harvv.com/v1/sites

Returns { "sites": [ ... ] }.

Site stats

GET /v1/sites/:id/stats

ParamValuesDefault
periodtoday, 7d, 30d7d

Anything else falls back to 7 days rather than erroring.

curl -H "Authorization: Bearer hv_live_xxx" \
  "https://harvv.com/v1/sites/SITE_ID/stats?period=30d"

Returns { "site": {...}, "period": "30d", "stats": {...} }.

Findings

GET /v1/sites/:id/issues

ParamValues
statusopen, quoted, approved, fixing, in_qa, verifying, resolved, merged, dismissed

With no status you get everything except dismissed. That default matters: dismissed findings are the largest group by a wide margin, so asking for "all findings" without it would bury the ones that need attention.

curl -H "Authorization: Bearer hv_live_xxx" \
  "https://harvv.com/v1/sites/SITE_ID/issues?status=open"

GET /v1/sites/:id/issues/:issue_id

One finding in full, including the plain-language title and the suggested fix. issue_id is numeric.

Friction elements

GET /v1/sites/:id/friction

The elements people click that do nothing, and the ones they click repeatedly. Top 15 of each, ranked by count.

ParamValuesDefault
period7d, 30d7d

Returns { "dead_clicks": [...], "rage_clicks": [...], "dashboard_url": "..." }.

API keys

These three take your dashboard session (a JWT), not an API key, because they are how you get an API key in the first place.

RouteWhat it does
GET /customer/api-keysList your keys. Never returns the secret again.
POST /customer/api-keysCreate a key. The only time the full value is returned.
DELETE /customer/api-keys/:idRevoke a key immediately.
curl -X POST -H "Authorization: Bearer JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"My integration"}' \
  https://harvv.com/customer/api-keys

Register a site

POST /v1/sites/register

Creates a site and returns its pixel key. Built for the WordPress plugin bootstrap, and usable by anything that provisions sites automatically. Needs an existing API key, so the account must already exist.

Reading the session ID

To tell us that a session reached a real outcome, your server needs to know which session it is confirming. Read it on the client and pass it through with whatever request you already make:

// direct
const sessionId = window.harvv?.sessionId;
const visitorId = window.harvv?.visitorId;

// or via accessors, if the pixel may not have loaded yet
const sessionId = window.harvv?.getSessionId?.();

Both IDs are 16 characters and both are random. The session ID covers one browsing session, ending after about 30 minutes idle. The visitor ID sits in a first-party cookie for around 30 days as a device signal. Neither is derived from anything about the person, and neither can be tied to a named individual without data you hold, so you can log both freely.

If you are validating or storing these, size the column for 16 characters. A varchar(8) silently truncates and makes different sessions collide, which is exactly the bug we shipped ourselves.

Outcomes

The pixel sees friction. It cannot see whether the person got what they came for. Telling us when they did is what separates "this annoyed people" from "this stopped people", and it changes how we rank the finding.

From the browser

window.harvv?.outcome('purchase_completed', {
  value: 99.00,
  currency: 'USD',
  step: 'individual',
  is_returning: true
});

The name is capped at 60 characters and anything outside a-z 0-9 _ : . - becomes an underscore. Up to 12 properties; string values are cut at 100 characters; numbers and booleans pass through as they are.

The whole event is dropped, silently, if any property name or value looks like personal data — an email address, a phone number, a card number, or a key that reads like one. It is dropped rather than scrubbed, on purpose: a partially cleaned event is worse than no event, because you would not know which half arrived. Send opaque IDs.

From your server

POST /v1/outcomes

The authoritative version, sent from your system of record.

curl -X POST https://harvv.com/v1/outcomes \
  -H "Authorization: Bearer hv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "site_id": "your-pixel-key",
    "session_id": "61b7df69a4c2e8f1",
    "outcome": "kyc_approved",
    "properties": { "approval_tier": "instant", "score": 87 }
  }'
FieldRequiredNotes
site_idYesYour pixel key, from the install snippet
session_idYesThe 16-character ID from the pixel
outcomeYesSnake case, up to 60 characters
timestampNoUnix milliseconds. Defaults to now
propertiesNoSame rejection rule as the browser call

What is already captured

Form submissions and same-origin API calls need no code from you. The pixel records form submit events, and fetch and XMLHttpRequest POSTs to paths that look like /api, /checkout, /submit, /signup.

It stores the path with numeric IDs and UUIDs collapsed to :id and :uuid, plus the response status. Never the request body, the response body, the headers, or the query string. When a session hits friction and then a submit succeeds within 30 seconds, we downgrade the finding to frustration rather than blockage.

For paths the heuristic misses, such as /complete or /finalize, call window.harvv.outcome(). An explicit outcome always beats the guess.

Privacy

Claude Code plugin

/plugin install github:AxiomState/harvv-claude-plugin
CommandWhat it does
/harvv:installPut the pixel in this project
/harvv:issuesList findings for the site
/harvv:fix 123Apply the suggested fix for finding 123
/harvv:whyFull conversion diagnosis

For conversational access to the same data, the MCP server is usually the better route.