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
Field
Type
Required
Limit
connector_id
string
required
Slug, 3 to 48 characters, such as my-mcp-server.
phrase
string
required
What the person asked. Aliases: user_intent, query. Raw text max 2,000 characters. Stored only after scrubbing, max 200 characters.
tool_name
string
required
1 to 64 characters. Letters, digits, underscore, dot, colon, or hyphen.
ts
string
required
ISO-8601 timestamp within two days of the server clock.
event_id
string
required
1 to 80 characters. Letters, digits, dot, underscore, colon, or hyphen. Repeats of the same id are duplicates.
platform
string
optional
chatgpt, muse, claude, grok, gemini, copilot, cursor, perplexity, mcp, api, or other. Unknown values are stored as other. Empty is left unset.
outcome
string
optional
success, no_result, or error. Any other value is dropped and the event is still accepted.
latency_ms
integer
optional
0 to 600000. Non-integers are dropped.
result_count
integer
optional
0 to 1000000. Non-integers are dropped.
country
string
optional
ISO 3166-1 alpha-2, such as US. Anything else is dropped.
session_id
string
optional
Conversation-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_version
string
optional
At most 32 characters. Letters, digits, dot, underscore, plus, or hyphen. Anything else is dropped.
is_test
boolean
optional
true marks a test. Tests are stored and do not count toward your totals or the shared view.
sample_rate
number
optional
The 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.
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()