Skip to main content

Pulse telemetry API

POST /api/v1/pulse/telemetry is the endpoint Clione Pulse — and any third-party scanner — uses to report a single page scan back to Clione. The endpoint is write-only: it accepts a payload, stores it, and returns 204 No Content. To read scans back, use the dashboard endpoints (auth-scoped) or GET /api/v1/public/pulse/badge for public badges.

For the full Pulse product, see Pulse dashboard and Pulse extension.


When to use this

You're integrating a non-Chrome scanner (CI step, Node script, custom crawler, partner tool) and want the results to appear in your Clione Pulse dashboard. The Chrome extension already calls this endpoint — you do not need to do anything to opt in there.

If you only want to read scans for a domain (badges, public scoreboards), see the public Pulse endpoints, not this one.


Endpoint

POST /api/v1/pulse/telemetry
Content-Type: application/json

Production base URL: https://api.clione.ai.

The endpoint responds with 204 No Content on success. Failure modes:

CodeMeaning
400Malformed JSON, missing required fields.
401Missing or invalid authentication headers.
401HMAC signature mismatch, stale timestamp (> 5 min skew), or revoked token.
500Persistence failed. The body contains { error, detail }.

Authentication — two modes

The endpoint accepts either of two mutually exclusive auth modes. Pick the one that matches your context.

Mode 1 — Public token (X-Pulse-Token)

Used by the public Chrome Web Store build of the Pulse extension and by anyone who signed up via the public Pulse landing page. Simplest mode.

Headers:

X-Pulse-Token: <PulseLead.apiToken>

How to get a token:

  1. Have your user sign up at the public Pulse landing page (or via the Chrome extension popup).
  2. The system issues an opaque apiToken tied to a PulseLead record.
  3. The token is verified, the apiToken is what you put in the header.

Token format: opaque 32+-byte random string. There is no HMAC — the token itself is the secret. Treat it like a password, do not commit it to source.

Rotation: contact your Clione superadmin. A rotated token immediately invalidates the old one.

Mode 2 — HMAC-signed API key (X-Pulse-Key + X-Pulse-Timestamp + X-Pulse-Signature)

Used by internal Clione tooling and by partners with a sk_live_* API key whose scopes include pulse:scan or admin. Required when you want the higher rate-limit tier.

Headers:

X-Pulse-Key: sk_live_<...>
X-Pulse-Timestamp: <epoch milliseconds, current time>
X-Pulse-Signature: <hex HMAC-SHA256>

Where:

  • X-Pulse-Timestamp is the current time in epoch milliseconds. The server rejects requests where |now - timestamp| > 5 minutes (replay window).
  • X-Pulse-Signature is HMAC-SHA256(apiKey, "${timestamp}.${rawJsonBody}"), hex-encoded. The raw JSON body — bytes as sent — is what's signed; do not re-stringify.

The server looks the API key up by its SHA-256 hash, then recomputes the HMAC using the raw key from the header. Use crypto.timingSafeEqual semantics on your end too if you build a verifier.


Payload

{
"url": "https://shop.example.com/products/widget-001",
"hostname": "shop.example.com",
"scannedAt": "2026-06-05T14:23:45.000Z",
"platform": { "platform": "shopify", "confidence": 0.95 },
"pageType": "product",
"clione": { "detected": true, "version": "0.4.1" },
"signals": {
"title": { "value": "Widget 001 — Acme", "length": 21 },
"description": { "value": "...", "length": 142 },
"canonical": { "value": "https://shop.example.com/products/widget-001" },
"robots": "index,follow",
"lang": "en",
"headings": { "h1": ["Widget 001"], "h2": ["Specifications"] },
"openGraph": {
"og:title": "Widget 001 — Acme",
"og:type": "product",
"og:image": "..."
},
"jsonLd": [
{ "@context": "https://schema.org", "@type": "Product", "name": "Widget 001", "sku": "W-001", "brand": { "name": "Acme" } }
]
},
"signalPresence": { "title": true, "description": true, "jsonLd": true, "openGraph": true, "h1": true, "canonical": true, "robots": true },
"quality": {
"grade": "B",
"percentage": 82,
"title": 2, "description": 2, "keywords": 1, "jsonLd": 2, "openGraph": 1
},
"country": "ES",
"vertical": "ecommerce"
}

Field reference (only required-for-persistence fields are listed as required; the rest are best-effort and degrade gracefully):

FieldRequiredTypeNotes
urlrecommendedstringThe full URL with path. Strip query string and hash before sending. If absent, the server falls back to https://${hostname}/.
hostnameyesstringBare host, no scheme, no path.
scannedAtrecommendedISO-8601Defaults to server now if omitted.
platform.platformrecommendedstringshopify, bigcommerce, woocommerce, magento, custom, unknown.
pageTyperecommendedstringproduct, collection, category, page, home, cart, checkout, unknown.
clione.detectedrecommendedbooleanTrue if the scanner found Clione's embed or schema fingerprint.
signalsyesobjectRaw extracted signals. The shape above mirrors what the Chrome extension sends.
signalPresencerecommendedobjectBoolean map. Used by the dashboard for fast presence aggregations.
quality.graderecommendedAFThe scanner's own grade. If absent, defaults to F.
quality.percentagerecommended0–100The scanner's own score. If absent, defaults to 0.
countryoptionalISO 3166-1 alpha-2Used by dashboard filters. If absent, the server uses GeoIP from the request IP.
verticaloptionalstringFree-form, used for tenant analytics segmentation.

Anything not listed is preserved in the raw data JSON blob — you can send arbitrary diagnostic fields and they'll persist for inspection, they just won't be indexed.


Curl examples

Mode 1 — Public token

curl -X POST https://api.clione.ai/api/v1/pulse/telemetry \
-H "Content-Type: application/json" \
-H "X-Pulse-Token: $PULSE_TOKEN" \
--data @scan-payload.json

Mode 2 — HMAC-signed API key

TS=$(node -e 'process.stdout.write(Date.now().toString())')
BODY=$(cat scan-payload.json)
SIG=$(node -e "
const c = require('crypto');
const ts = process.argv[1];
const body = process.argv[2];
const key = process.env.PULSE_API_KEY;
process.stdout.write(c.createHmac('sha256', key).update(\`\${ts}.\${body}\`).digest('hex'));
" "$TS" "$BODY")

curl -X POST https://api.clione.ai/api/v1/pulse/telemetry \
-H "Content-Type: application/json" \
-H "X-Pulse-Key: $PULSE_API_KEY" \
-H "X-Pulse-Timestamp: $TS" \
-H "X-Pulse-Signature: $SIG" \
--data "$BODY"

A successful call returns 204 No Content with an empty body.


Rate limits

This endpoint inherits the same tier-based rate limits as the rest of /api/v1/pulse/*:

TierHow recognizedScans per minute
anonymousno auth headers, or unrecognized token10
authedvalid X-Pulse-Token for a verified lead50
agentX-Pulse-Key with scope pulse:scan or admin1000

429 Too Many Requests includes a JSON body indicating the active tier and the limit so you can back off appropriately.


Idempotency and dedup

The endpoint is not idempotent — sending the same payload twice creates two PulseScan rows. This is intentional: a re-scan after a deploy is a real event.

If you need dedup, dedupe on the client side, or use the bulk endpoint (POST /pulse/scan-bulk) which performs skipDuplicates on (url, scannedAt) insert.