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.
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.
# 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 — 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: 3the SDK uses no eval and loads no remote code. -
storage,alarms,unlimitedStoragepermissions: events accumulate locally and go out as one bucket per day. -
webNavigation(or tabs) — the source of navigation events. -
api.intentd.netin 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
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
| 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 |
| 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.
| 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
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.
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) 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.