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
| Requests | Limit |
|---|---|
| Authenticated (any request carrying a bearer token) | 300 per minute per IP |
| Unauthenticated | 60 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
| Param | Values | Default |
|---|---|---|
period | today, 7d, 30d | 7d |
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
| Param | Values |
|---|---|
status | open, 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.
| Param | Values | Default |
|---|---|---|
period | 7d, 30d | 7d |
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.
| Route | What it does |
|---|---|
GET /customer/api-keys | List your keys. Never returns the secret again. |
POST /customer/api-keys | Create a key. The only time the full value is returned. |
DELETE /customer/api-keys/:id | Revoke 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 }
}'
| Field | Required | Notes |
|---|---|---|
site_id | Yes | Your pixel key, from the install snippet |
session_id | Yes | The 16-character ID from the pixel |
outcome | Yes | Snake case, up to 60 characters |
timestamp | No | Unix milliseconds. Defaults to now |
properties | No | Same 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
- No form values, request bodies, response bodies, cookies, headers or query strings are read on observed requests.
- Nothing inside
data-harvv-private="true", a password, email, phone or hidden input, or a contenteditable region is captured. - Outcome events containing anything that looks like personal data are dropped whole.
- Session and visitor IDs are random and not linkable to a person without your own data.
Claude Code plugin
/plugin install github:AxiomState/harvv-claude-plugin
| Command | What it does |
|---|---|
/harvv:install | Put the pixel in this project |
/harvv:issues | List findings for the site |
/harvv:fix 123 | Apply the suggested fix for finding 123 |
/harvv:why | Full conversion diagnosis |
For conversational access to the same data, the MCP server is usually the better route.