Skip to content

Documentation

IntentD integration and data format

Technical specifications for extension developers: connecting the SDK, the Ingest API, limits and the delivery format your data arrives in.

Manifest V3 HMAC-SHA256 X25519 + AES-GCM Parquet + LZ4 PII-free

01 · Publishers

SDK Quickstart

The SDK runs in the extension background script and needs no access to page DOM. The bundle is built in the dashboard specifically for your extension: the keys, the encryption key and the extension ID are already inside, and there is no init to call by hand. Before the user consents the SDK collects nothing — how the consent prompt works.

bash
# Dashboard → Extensions → SDK → Download
# The intentd-<name>.min.js file already carries your keys and configuration.
cp ~/Downloads/intentd-my-extension.min.js ./extension/intentd.min.js
background.js
// background.js — first line, at the top level.
// init is called for you; listeners must be registered when the service
// worker starts, otherwise Manifest V3 will not deliver the events.
import './intentd.min.js';

// Optional: status and a manual flush.
// const status = await IntentD.status();
// await IntentD.flush();

Manifest requirements

  • manifest_version: 3 the SDK uses no eval and loads no remote code.
  • storage, alarms, unlimitedStorage permissions: events accumulate locally and go out as one bucket per day.
  • webNavigation (or tabs) — the source of navigation events.
  • api.intentd.net in host_permissions.

Before downloading, enter your store extension ID in the dashboard: the bundle refuses to run inside any other extension and says so.

02 · Publishers

Ingest API

Base URL
https://api.intentd.net/v1
Authentication
X-API-Key + HMAC-SHA256 over the body

Endpoints

Method Path Purpose Limit
POST /sdk/register Registers an installation: the session and the bundle identifier. Idempotent. 120 requests / min per IP
POST /telemetry/bucket An encrypted event bucket (X25519 + AES-256-GCM). Once a day, or earlier. 48 buckets / day per installation
POST /sdk/report-tamper The SDK reporting a failed integrity check. 30 requests / min per IP

Example request

http
POST /v1/telemetry/bucket HTTP/1.1
Host: api.intentd.net
Content-Type: application/json
X-API-Key: ext_...
X-Timestamp: 1758000000
# hex(HMAC-SHA256(secret, "intentd-v2|bucket|<X-Timestamp>|<X-API-Key>|" + hex(SHA-256(body))))
X-Signature: d49d3b20...

{
  "bucket_id": "7c0e…", "session_id": "2b91…",
  "ephemeral_public_key": "base64(32 bytes)",
  "iv": "base64(12 bytes)",
  "encrypted_key": "base64(48 bytes)",
  "ciphertext": "base64(AES-256-GCM of the INTD bucket)",
  "events_count": 1234, "bytes_size": 45678, "bucket_version": 1,
  "client_ts": 1758000000, "fingerprint_hash": "5c9a96…"
}

Error codes

HTTP Code Meaning
400 BUCKET_INVALID_FORMAT The bucket did not decrypt, or failed the format and CRC32 check
401 API_KEY_REVOKED The key is revoked — the SDK switches itself off
401 INVALID_SIGNATURE The HMAC signature did not match the request body
401 CLOCK_SKEW The client clock is more than 5 minutes out; the response carries server_time
402 SUBSCRIPTION_INACTIVE No active subscription — events stay in the queue
402 SUBSCRIPTION_LIMIT_REACHED The plan limit for the current month is exhausted. Events stay queued on the client until the plan is upgraded.
403 WRONG_EXTENSION The bundle is running in an extension it was not issued for
403 FINGERPRINT_MISMATCH The environment does not match the session — the SDK registers again
409 BUCKET_ALREADY_RECEIVED This bucket has already been accepted
413 BUCKET_TOO_LARGE More than 10,000 events or 10 MB — the SDK splits the bucket
429 RATE_LIMITED The request rate or the daily bucket limit was exceeded

On network errors, 402, 429 and 503 the SDK keeps the queue and retries once an hour. The binary bucket format and the encryption scheme are documented openly — the INTD v1 specification.

Publishers

Rate limits and quotas

Per installation
POST /sdk/register 120 requests / min per IP
POST /telemetry/bucket 48 buckets / day per installation, 120 requests / min per IP
POST /sdk/report-tamper 30 requests / min per IP
Per plan (clean events ingested per calendar month, UTC)
Starter 100,000
Growth 1,000,000
Scale 10,000,000
Enterprise Custom

Once the monthly limit is reached, new buckets are refused with 402 SUBSCRIPTION_LIMIT_REACHED and stay queued on the client. Sending resumes after an upgrade, or on the first day of the next month. The bucket that crosses the limit is still accepted.

03 · Data partners

Data schema

One Parquet row is one cleaned event. There are no personal-data fields in the schema by construction: they are dropped in the browser, before anything is sent.

Event fields in the Parquet delivery
Field Type Description
event_id String Unique GUID of the event
timestamp Int64 Unix timestamp in milliseconds, UTC
anon_uid String HMAC hash of the user session, irreversible
clean_url String The URL after PII parameters are stripped
search_query String The extracted search intent, when there is one
lang String Browser language, BCP 47
geo_country String ISO country code, resolved from the IP at the edge
device_type String desktop / mobile / tablet
domain String Root domain, without subdomains

Data partners

Data Provenance

  • Data Buyer access is in development. The technical specs describe the delivery format we are building. Access is not yet available.
  • Every event comes only from users who explicitly pressed “Allow” in the consent prompt (GDPR art. 6(1)(a)). Before consent the SDK creates no identifier and stores no events.
  • Each accepted bucket carries the version of the consent text; the server rejects buckets without it and records the version in the intake log. When the text changes, consent is asked again.
  • When consent is withdrawn the SDK stops collecting and deletes unsent events on the device; a later grant creates a new pseudonym unrelated to the previous one.
  • Whether and when a particular user consented is not stored on the server: the accepted bucket and the recorded text version are the evidence.

04 · Data partners

Storage and delivery

Format
Parquet + LZ4
Storage
AWS S3
Hot access
30 days
Archive
90 days

Path structure

s3
year=YYYY/month=MM/day=DD/tenant=<key_id>__<bucket_id>.parquet.lz4

Partitioning by date and tenant lets you read the slice you need without scanning the whole bucket. Syncing into your own bucket is available on every plan.

05 · Data partners

Reading examples

The delivery reads with any standard Parquet stack. Below are minimal examples in Python and Go.

python
import pandas as pd

# A tenant's daily partition in S3.
path = "s3://intentd-delivery/year=2026/month=09/day=11/tenant=ext_acme__7c0e.parquet.lz4"

frame = pd.read_parquet(
    path,
    columns=["timestamp", "domain", "search_query", "geo_country"],
    storage_options={
        "key": "YOUR_ACCESS_KEY",
        "secret": "YOUR_SECRET_KEY",
    },
)

top_domains = frame.groupby("domain").size().sort_values(ascending=False).head(20)
print(top_domains)
go
package main

import (
    "context"
    "log"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
    ctx := context.Background()

    cfg, err := config.LoadDefaultConfig(ctx)
    if err != nil {
        log.Fatal(err)
    }

    client := s3.NewFromConfig(cfg)

    object, err := client.GetObject(ctx, &s3.GetObjectInput{
        Bucket: ptr("intentd-delivery"),
        Key:    ptr("year=2026/month=09/day=11/tenant=ext_acme__7c0e.parquet.lz4"),
    })
    if err != nil {
        log.Fatal(err)
    }
    defer object.Body.Close()

    // From here on, any Parquet reader will do -- parquet-go, for example.
    log.Printf("received %d bytes", *object.ContentLength)
}

func ptr(s string) *string { return &s }

Ready to connect?

Create an account to get your keys and download the SDK. If you have a technical question before you start, write to us.

The full OpenAPI specification of the portal lives in the repository: portal/docs/swagger.yaml. A public hosted version will arrive with the dashboard — tell us you need it.

Talk to us