openapi: 3.1.0 # generated: '2026-07-20' # method: generated # source: https://docs.millimetric.ai/api-reference/overview.md (+ per-endpoint reference pages) # note: API Evangelist faithfully transcribed this OpenAPI from Millimetric's published # API reference. Millimetric does not (yet) publish an OpenAPI document; every # operation, parameter, schema, and error below is taken verbatim from the docs. info: title: Millimetric Analytics API version: v1 description: >- API-first, privacy-respecting analytics for developers, small startups, and AI agents. Capture events, identify users, run GDPR deletes, and query aggregated stats and attribution — no cookies, no dashboards required. All access is via Bearer API keys (pk_/sk_/rk_/ak_ kinds). Endpoints are versioned under /v1/*. contact: name: Millimetric Support email: hello@millimetric.ai url: https://docs.millimetric.ai x-apievangelist-generated: true servers: - url: https://api.millimetric.ai description: Production tags: - name: Ingest description: Write events into a project. - name: Identity description: Link anonymous visitors to known users and honor deletion requests. - name: Read description: Query raw events and aggregated stats and attribution. paths: /v1/track: post: operationId: trackEvent summary: Emit a single analytics event description: >- Emit a single analytics event — the most common write endpoint. Requires an ingest-scope key (pk_* browser, origin-checked; or sk_* server). The server enriches every event with source/medium classification, geo, device, and a hashed IP before insert. tags: [Ingest] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TrackEventInput' example: event: purchase event_id: evt_abc123 anonymous_id: u_abc user_id: user_42 properties: amount_cents: 4900 currency: usd responses: '202': description: Accepted — event written to ClickHouse synchronously. content: application/json: schema: type: object properties: ok: { type: boolean } event_id: { type: string } example: { ok: true, event_id: evt_abc123 } '400': { $ref: '#/components/responses/InvalidPayload' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } '500': { $ref: '#/components/responses/ServerError' } /v1/batch: post: operationId: batchEvents summary: Emit up to 1000 events in one call description: >- Send up to 1000 events in a single request. All-or-nothing at the request level — if validation fails on any event, none are written. Used by the browser SDK for buffered flushes. Requires an ingest-scope key (pk_* or sk_*). tags: [Ingest] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BatchInput' responses: '202': description: Accepted — all events written. content: application/json: schema: type: object properties: ok: { type: boolean } count: { type: integer } example: { ok: true, count: 2 } '400': { $ref: '#/components/responses/InvalidPayload' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } '500': { $ref: '#/components/responses/ServerError' } /v1/identify: post: operationId: identify summary: Link an anonymous_id to a user_id description: >- Bind a known user_id to a visitor's anonymous_id. Persists as a special $identify event so pre- and post-login behavior can be stitched together. Requires an ingest-scope key (pk_* or sk_*). tags: [Identity] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/IdentifyInput' responses: '200': description: OK content: application/json: schema: type: object properties: ok: { type: boolean } example: { ok: true } '400': { $ref: '#/components/responses/InvalidPayload' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /v1/forget: post: operationId: forget summary: GDPR delete for a user description: >- Permanently delete every event for (project_id, user_id). Intended for GDPR / CCPA Right-to-be-Forgotten. sk_* keys only — pk_* is explicitly rejected to prevent a leaked browser key from wiping data. tags: [Identity] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ForgetInput' responses: '200': description: Deletion queued on ClickHouse. content: application/json: schema: type: object properties: ok: { type: boolean } queued: { type: boolean } example: { ok: true, queued: true } '401': { $ref: '#/components/responses/Unauthorized' } '403': description: forget_requires_secret_key — a pk_* key was used. content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: forget_requires_secret_key } '500': description: forget_failed — ClickHouse rejected the mutation. content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: forget_failed } /v1/query: get: operationId: queryEvents summary: Return raw events filtered by time range description: >- Return raw event rows matching a time range and optional filters. For debugging, ad-hoc inspection, and small reports. Requires a read-scope key (rk_*). tags: [Read] security: - bearerAuth: [] parameters: - { name: from, in: query, required: true, schema: { type: string, format: date-time }, description: ISO 8601 timestamp (inclusive). } - { name: to, in: query, required: true, schema: { type: string, format: date-time }, description: ISO 8601 timestamp (exclusive). } - { name: event, in: query, required: false, schema: { type: string }, description: Filter to one event name. } - { name: source, in: query, required: false, schema: { type: string }, description: Filter to one source. } - { name: medium, in: query, required: false, schema: { type: string }, description: Filter to one medium. } - { name: user_id, in: query, required: false, schema: { type: string }, description: Filter to events tagged with this user_id. } - { name: limit, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 1000, default: 100 } } responses: '200': description: Matching event rows. content: application/json: schema: type: object properties: rows: type: array items: { $ref: '#/components/schemas/EventRow' } '400': { $ref: '#/components/responses/InvalidParams' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /v1/stats: get: operationId: getStats summary: Aggregations over events description: >- Aggregate counts or unique-visitor counts (HyperLogLog) over a time range, optionally grouped and/or time-bucketed. Requires a read-scope key (rk_*). tags: [Read] security: - bearerAuth: [] parameters: - { name: metric, in: query, required: false, schema: { type: string, enum: [count, uniques], default: count } } - { name: from, in: query, required: true, schema: { type: string, format: date-time } } - { name: to, in: query, required: true, schema: { type: string, format: date-time } } - { name: event, in: query, required: false, schema: { type: string } } - name: group_by in: query required: false description: 'Comma-separated columns. Allowed: event_name, source, medium, country, device_type, browser, os, path.' schema: { type: string } - { name: interval, in: query, required: false, schema: { type: string, enum: [hour, day, week] } } - { name: limit, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 1000, default: 100 } } responses: '200': description: Aggregated rows. content: application/json: schema: type: object properties: metric: { type: string } rows: type: array items: { type: object, additionalProperties: true } example: metric: count rows: - { source: facebook, medium: paid, value: 6 } - { source: facebook, medium: social, value: 3 } '400': { $ref: '#/components/responses/InvalidParams' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /v1/sources: get: operationId: getSources summary: Top sources/mediums with the paid-vs-social split description: >- Top sources (and optionally source x medium) for the project — the endpoint that surfaces the Facebook social-vs-paid split. Requires a read-scope key (rk_*). tags: [Read] security: - bearerAuth: [] parameters: - { name: from, in: query, required: true, schema: { type: string, format: date-time } } - { name: to, in: query, required: true, schema: { type: string, format: date-time } } - { name: breakdown, in: query, required: false, schema: { type: string, enum: [source_medium, source], default: source_medium } } - { name: limit, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } } responses: '200': description: Top-source rows. content: application/json: schema: type: object properties: breakdown: { type: string } rows: type: array items: type: object properties: source: { type: string } medium: { type: string } events: { type: integer } uniques: { type: integer } paid_share: { type: number } example: breakdown: source_medium rows: - { source: facebook, medium: paid, events: 6, uniques: 6, paid_share: 1.0 } - { source: facebook, medium: social, events: 3, uniques: 3, paid_share: 0.0 } '400': { $ref: '#/components/responses/InvalidParams' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } components: securitySchemes: bearerAuth: type: http scheme: bearer description: >- Bearer API key of the form {kind}_{env}_{prefix}_{secret}. Kinds: pk_ (browser, ingest, origin-allowlisted), sk_ (server, ingest+read), rk_ (read only, recommended for agents/MCP), ak_ (account-level read across projects, Business tier). schemas: TrackEventInput: type: object required: [event] properties: event: { type: string, minLength: 1, maxLength: 128, description: 'Event name. $-prefix reserved for system events.' } event_id: { type: string, minLength: 1, maxLength: 128, description: Idempotency / join key. } timestamp: { type: string, format: date-time } anonymous_id: { type: string, minLength: 1, maxLength: 128 } user_id: { type: string, minLength: 1, maxLength: 256 } session_id: { type: string, minLength: 1, maxLength: 128 } url: { type: string, maxLength: 2048 } path: { type: string, maxLength: 2048 } referrer: { type: string, maxLength: 2048 } properties: { type: object, additionalProperties: true, description: Free-form JSON, capped at 8 KB stringified. } BatchInput: type: object required: [events] properties: events: type: array minItems: 1 maxItems: 1000 items: { $ref: '#/components/schemas/TrackEventInput' } IdentifyInput: type: object required: [anonymous_id, user_id] properties: anonymous_id: { type: string, minLength: 1, maxLength: 128 } user_id: { type: string, minLength: 1, maxLength: 256 } traits: { type: object, additionalProperties: true } ForgetInput: type: object required: [user_id] properties: user_id: { type: string, minLength: 1, maxLength: 256 } EventRow: type: object properties: timestamp: { type: string, format: date-time } event_name: { type: string } anonymous_id: { type: string } user_id: { type: string, nullable: true } source: { type: string } medium: { type: string } campaign: { type: string, nullable: true } source_confidence: { type: string, enum: [low, medium, high] } referrer: { type: string, nullable: true } url: { type: string, nullable: true } path: { type: string, nullable: true } country: { type: string } device_type: { type: string } properties: { type: string, description: JSON-encoded string. } Error: type: object required: [error] properties: error: { type: string, description: Stable machine-readable error code. } details: { type: object, additionalProperties: true } responses: InvalidPayload: description: 400 invalid_payload — body failed validation. content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: invalid_payload, details: { fieldErrors: { event: [Required] } } } InvalidParams: description: 400 invalid_params / invalid_group_by — query string failed validation. content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: invalid_params } Unauthorized: description: 401 — missing or invalid Bearer key. content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: invalid_api_key } Forbidden: description: 403 — origin_not_allowed / insufficient_scope. content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: insufficient_scope } RateLimited: description: 429 rate_limited — token bucket exhausted. Retry-After header included. headers: Retry-After: schema: { type: integer } description: Seconds to wait before retrying. content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: rate_limited, retry_after_s: 1 } ServerError: description: 500 internal_error / 502 upstream_failed. content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: internal_error }