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:
| Code | Meaning |
|---|---|
400 | Malformed JSON, missing required fields. |
401 | Missing or invalid authentication headers. |
401 | HMAC signature mismatch, stale timestamp (> 5 min skew), or revoked token. |
500 | Persistence 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:
- Have your user sign up at the public Pulse landing page (or via the Chrome extension popup).
- The system issues an opaque
apiTokentied to aPulseLeadrecord. - 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-Timestampis the current time in epoch milliseconds. The server rejects requests where|now - timestamp| > 5 minutes(replay window).X-Pulse-SignatureisHMAC-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):
| Field | Required | Type | Notes |
|---|---|---|---|
url | recommended | string | The full URL with path. Strip query string and hash before sending. If absent, the server falls back to https://${hostname}/. |
hostname | yes | string | Bare host, no scheme, no path. |
scannedAt | recommended | ISO-8601 | Defaults to server now if omitted. |
platform.platform | recommended | string | shopify, bigcommerce, woocommerce, magento, custom, unknown. |
pageType | recommended | string | product, collection, category, page, home, cart, checkout, unknown. |
clione.detected | recommended | boolean | True if the scanner found Clione's embed or schema fingerprint. |
signals | yes | object | Raw extracted signals. The shape above mirrors what the Chrome extension sends. |
signalPresence | recommended | object | Boolean map. Used by the dashboard for fast presence aggregations. |
quality.grade | recommended | A–F | The scanner's own grade. If absent, defaults to F. |
quality.percentage | recommended | 0–100 | The scanner's own score. If absent, defaults to 0. |
country | optional | ISO 3166-1 alpha-2 | Used by dashboard filters. If absent, the server uses GeoIP from the request IP. |
vertical | optional | string | Free-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/*:
| Tier | How recognized | Scans per minute |
|---|---|---|
| anonymous | no auth headers, or unrecognized token | 10 |
| authed | valid X-Pulse-Token for a verified lead | 50 |
| agent | X-Pulse-Key with scope pulse:scan or admin | 1000 |
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.
Related
- Pulse dashboard — the merchant-facing aggregator.
- Pulse extension — Chrome extension that calls this endpoint by default.
- API authentication — for issuing
sk_live_*keys withpulse:scanscope.