LockstepsConnector Analytics

API

Ingest API reference

POST JSON to https://www.locksteps.ai/v1/analytics/ingest with Authorization: Bearer and your key. The key must belong to the connector_id in the body. Keys use a dsn_ prefix. A missing, malformed, or unknown key returns 401 with the same body, so callers cannot tell those cases apart.

Fields

FieldTypeRequiredLimit
connector_idstringrequiredSlug, 3 to 48 characters, such as my-mcp-server.
phrasestringrequiredWhat the person asked. Aliases: user_intent, query. Raw text max 2,000 characters. Stored only after scrubbing, max 200 characters.
tool_namestringrequired1 to 64 characters. Letters, digits, underscore, dot, colon, or hyphen.
tsstringrequiredISO-8601 timestamp within two days of the server clock.
event_idstringrequired1 to 80 characters. Letters, digits, dot, underscore, colon, or hyphen. Repeats of the same id are duplicates.
platformstringoptionalchatgpt, muse, claude, grok, gemini, copilot, cursor, perplexity, mcp, api, or other. Unknown values are stored as other. Empty is left unset.
outcomestringoptionalsuccess, no_result, or error. Any other value is dropped and the event is still accepted.
latency_msintegeroptional0 to 600000. Non-integers are dropped.
result_countintegeroptional0 to 1000000. Non-integers are dropped.
countrystringoptionalISO 3166-1 alpha-2, such as US. Anything else is dropped.
session_idstringoptionalConversation-scoped opaque id, max 128. Reuse across tool calls in one chat. If omitted, server mints one and returns it. Hashed at rest. Needed for FREE conversions.
connector_versionstringoptionalAt most 32 characters. Letters, digits, dot, underscore, plus, or hyphen. Anything else is dropped.
is_testbooleanoptionaltrue marks a test. Tests are stored and do not count toward your totals or the shared view.
sample_ratenumberoptionalThe fraction of events you kept, greater than 0 and at most 1. The dashboard multiplies counts by 1 divided by this rate. Invalid values are dropped.

Success

Stored: { "ok": true, "stored": true, "junk": false, "is_test": false, "counted": true, "session_id": "…" }. Duplicate event id or the same scrubbed phrase in the same minute: { "ok": true, "stored": false, "duplicate": "event_id" } or phrase_minute. Duplicates are not errors.

Conversions (FREE)

In-plugin only. POST JSON to https://www.locksteps.ai/v1/analytics/conversion with the same bearer key. Required: connector_id, session_id (from ingest), event_name (e.g. lead_submitted, purchase_completed). Optional: event_id, value, currency, metadata (no PII), ts. Stitches to the latest phrase event for that session on this connector. Dashboard shows conversion counts and per-phrase rates.

Node / TypeScript

// FREE. Reuse session_id from ingest (same chat / conversation).
await fetch("https://www.locksteps.ai/v1/analytics/conversion", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    connector_id: "YOUR_CONNECTOR_ID",
    session_id: sessionId,
    event_name: "lead_submitted", // or purchase_completed
    // value: 49.0,
    // currency: "USD",
    // metadata: { plan: "pro" }, // no PII
  }),
});

Python

# FREE. Reuse session_id from ingest (same chat / conversation).
import json, urllib.request

req = urllib.request.Request(
    "https://www.locksteps.ai/v1/analytics/conversion",
    data=json.dumps({
        "connector_id": "YOUR_CONNECTOR_ID",
        "session_id": session_id,
        "event_name": "purchase_completed",
        # "value": 49.0,
        # "currency": "USD",
    }).encode(),
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    },
    method="POST",
)
urllib.request.urlopen(req, timeout=2).read()

Limits

Each API key can burst up to 600 events, then about 2 events per second. An organization can store 100,000 counted events per day. The day resets at 00:00 UTC. During its first 7 days, an organization can store 10,000 counted events per day and use two active keys. Counted events are stored phrases that are not tests and were not flagged.

If your server averages more than about 1 event per second, send a sample instead of every event. Set sample_rate to the fraction you keep, such as 0.1 when you send 1 event in 10, so the dashboard can scale the counts. A higher limit is available on request.

Errors

  • 401 unauthorized. No bearer token, token is not a key, key does not match the connector, or the key is revoked.
  • 400 invalid_request. Body is not JSON, or a required field is missing or the wrong shape.
  • 413 invalid_request. The body is larger than 8 KB.
  • 429 rate_limited. The key, the organization, or the platform is over its limit. The body includes retry_after_seconds and the response sets Retry-After. Wait that many seconds before sending again. Sending sooner only creates more rejected requests.
  • 503 not_configured. The analytics store is not reachable from this server.
  • 500 internal_error. Unexpected failure. The event was not confirmed stored.

An invalid optional field is dropped. It does not turn the request into a 400.

Scrubbing

Before a phrase is stored, the server removes URLs, email addresses, phone numbers, digit runs of 6 or more, tokens that look like tracking numbers, honorifics with names, and capitalized name pairs. Remaining digits are removed. The result is lowercased, punctuation is stripped, and it is cut at 200 characters. If fewer than two letters remain, the stored phrase is empty. The raw phrase is not kept.

The session id is hashed on receipt and the raw value is discarded. IP addresses and user agents are not fields on the event and are not stored by this API.

What counts

A counted event has a stored phrase, is not a test, and was not flagged as junk. Junk checks cover very short text, lorem or obvious test strings, the same phrase repeated, a burst of events, evenly spaced events, and a high duplicate ratio. Flagged events are stored and left out of totals and the shared view.

GET this URL returns a short JSON description. OPTIONS answers a CORS preflight for browser calls. GET /v1/analytics/health reports whether the store is reachable.

Minimal event

const INGEST_URL = "https://www.locksteps.ai/v1/analytics/ingest";

// phrase is what the person asked. user_intent and query are accepted aliases.
export async function logConnectorEvent(input: {
  connectorId: string;
  apiKey: string;
  phrase: string;
  toolName: string;
  eventId: string;
  platform?: string;
}) {
  await fetch(INGEST_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${input.apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      connector_id: input.connectorId,
      platform: input.platform,
      phrase: input.phrase,
      tool_name: input.toolName,
      ts: new Date().toISOString(),
      event_id: input.eventId,
    }),
  });
}

Richer event

const INGEST_URL = "https://www.locksteps.ai/v1/analytics/ingest";

// phrase is what the person asked. user_intent and query are accepted aliases. Optional fields are omitted when you do not have them.
export async function logConnectorEvent(input: {
  connectorId: string;
  apiKey: string;
  phrase: string;
  toolName: string;
  eventId: string;
  platform?: string;
  outcome?: "success" | "no_result" | "error";
  latencyMs?: number;
  resultCount?: number;
  country?: string;
  sessionId?: string;
  connectorVersion?: string;
}) {
  await fetch(INGEST_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${input.apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      connector_id: input.connectorId,
      platform: input.platform,
      phrase: input.phrase,
      tool_name: input.toolName,
      ts: new Date().toISOString(),
      event_id: input.eventId,
      outcome: input.outcome,
      latency_ms: input.latencyMs,
      result_count: input.resultCount,
      country: input.country,
      session_id: input.sessionId,
      connector_version: input.connectorVersion,
    }),
  });
}

Python richer event

import json
from datetime import datetime, timezone
from urllib.request import Request, urlopen

INGEST_URL = "https://www.locksteps.ai/v1/analytics/ingest"

# phrase is what the person asked. user_intent and query are accepted aliases. Optional fields are omitted when you do not have them.
def log_connector_event(
    connector_id,
    api_key,
    phrase,
    tool_name,
    event_id,
    platform=None,
    outcome=None,
    latency_ms=None,
    result_count=None,
    country=None,
    session_id=None,
    connector_version=None,
):
    payload = {
        "connector_id": connector_id,
        "platform": platform,
        "phrase": phrase,
        "tool_name": tool_name,
        "ts": datetime.now(timezone.utc).isoformat(),
        "event_id": event_id,
        "outcome": outcome,
        "latency_ms": latency_ms,
        "result_count": result_count,
        "country": country,
        "session_id": session_id,
        "connector_version": connector_version,
    }
    request = Request(
        INGEST_URL,
        data=json.dumps({key: value for key, value in payload.items() if value is not None}).encode(),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    with urlopen(request, timeout=2) as response:
        response.read()

curl

curl -sS https://www.locksteps.ai/v1/analytics/ingest \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "connector_id": "YOUR_CONNECTOR_ID",
    "platform": "api",
    "phrase": "where is the package status",
    "tool_name": "track_package",
    "ts": "2026-10-04T12:00:00.000Z",
    "event_id": "evt-test-1",
    "is_test": true
  }'

OpenAPI: /openapi.yaml. A thin logger for TypeScript and Python is prepared in this repo and is not published yet. The snippets above work today.

More

Questions: hi@locksteps.ai