Server-side event ingest
The VEKTIS tracker SDK is a thin client over a single HTTP endpoint. If you need to send events from a backend service, a batch job, a mobile-server pipeline, or anything else that isn’t a browser, you can call that endpoint directly.
This page is the HTTP reference. The browser SDK is documented under VEKTIS Tracker — pick that if your events originate on the client.
Endpoint
Section titled “Endpoint”POST https://events.vektis.io/api/v1/eventsContent-Type: application/jsonX-Vektis-Key: vk_prd_...Content-Type: application/json is required. Requests without it return 415 Unsupported Media Type.
Authentication
Section titled “Authentication”Pass your VEKTIS API key in the X-Vektis-Key header. Direct (non-browser) callers should always use the header.
For browsers using navigator.sendBeacon() on page-unload — where custom headers can’t be set — a ?key=<key> query-param fallback is accepted. The browser SDK uses this path automatically; server callers should not.
For server-side calls, use a Full (server-side) key (prefix vk_<env>_..., e.g. vk_prd_... for production), created on the API Keys page. The browser-safe Publishable keys (vk_pub_<env>_...) that the tracker SDK uses are also accepted, but they carry tighter rate limits — prefer a Full key from a backend caller.
Request body
Section titled “Request body”The body is always a events array, even for a single event:
{ "events": [ /* 1 to 100 TrackingEvent objects */ ]}| Constraint | Value |
|---|---|
| Minimum events per request | 1 |
| Maximum events per request | 100 |
| Maximum body size | 512 KB |
Per-event fields
Section titled “Per-event fields”| Field | Required | Type | Notes |
|---|---|---|---|
event_id | yes | string (UUID) | Caller-generated UUID. Generate this when the event happens, not on retry — see Idempotency. |
event_type | yes | enum | One of the five values in Event types. Other values are rejected. |
customer_id | yes | string, 1–255 chars | Top-level on each event, not inside properties. |
feature_id | required for feature.* events | string, 1–255 chars | feature.used, feature.engagement, and feature.first_use all require this. |
user_id | no | string, ≤255 chars | Top-level on each event, not inside properties. |
action | no | string, ≤255 chars | Free-form discriminator within an event_type (e.g., "started", "completed"). |
properties | no | object | See constraints below. |
timestamp | no | ISO 8601 with offset (e.g., 2026-05-25T14:30:00Z) | Must fall within 7 days in the past to 1 hour in the future of server time. If omitted, the server stamps the event on receipt. |
properties constraints
Section titled “properties constraints”| Constraint | Value |
|---|---|
| Maximum keys | 50 |
| Maximum key length | 64 characters |
| Maximum string value length | 1024 characters |
| Maximum total JSON size | 8 KB |
| Allowed value types | string, number, boolean |
Event types
Section titled “Event types”event_type is a fixed enum. Other values return 400 Bad Request.
event_type | Notes |
|---|---|
feature.used | Primary event — a customer used a feature. Requires feature_id. |
feature.engagement | Engagement signal within a feature (e.g., depth of use). Requires feature_id. |
feature.first_use | First-time-use signal for a customer/feature pair. Requires feature_id. |
session.active | Active-session signal, used to anchor active-user metrics. |
customer.identified | Associates a customer_id with optional user_id. Non-billable — does not count against your event quota. |
Server-side identify
Section titled “Server-side identify”There is no dedicated /identify endpoint. To identify a customer from a server, post a customer.identified event:
{ "events": [{ "event_id": "550e8400-e29b-41d4-a716-446655440000", "event_type": "customer.identified", "customer_id": "acct_A1", "user_id": "user_123" }]}Responses
Section titled “Responses”Success: 202 Accepted
Section titled “Success: 202 Accepted”{ "accepted": 3 }accepted is the number of events that passed validation and were queued for ingestion. It is not a count of stored rows — duplicates by event_id are dropped silently during async processing (see Idempotency).
Errors
Section titled “Errors”All errors return JSON with at least statusCode and message:
{ "statusCode": 400, "message": "Validation failed", "errors": [ /* per-field issues */ ]}| Status | When | Notable body fields |
|---|---|---|
400 Bad Request | Schema validation failed, or the body was not valid JSON | errors[] (per-field issues, present for schema failures) |
401 Unauthorized | API key is missing or invalid | — |
413 Payload Too Large | Request body exceeded 512 KB | — |
415 Unsupported Media Type | Content-Type was not application/json | — |
429 Too Many Requests | Rate limit exceeded | retryAfter (seconds); also returned as the Retry-After HTTP header |
500 Internal Server Error | Unhandled server error | — |
Idempotency
Section titled “Idempotency”The server deduplicates events by event_id scoped to your account. Posting the same event_id twice results in the duplicate being dropped silently — no error, no double-count.
Two implications:
acceptedreflects queued events, not stored rows. If you re-send a batch after a network blip, the response can showaccepted: 100while none of those events are newly stored.- Generate
event_idwhen the event happens, not on retry. A fresh UUID per retry defeats dedupe; a stable UUID per event makes retries safe.
Rate limits
Section titled “Rate limits”Per-IP and per-API-key rate limits are enforced. When you exceed a limit you receive 429 with a Retry-After header (seconds). Honor Retry-After before resending.
Limits are not published numerically and may change. The SDK respects Retry-After automatically; direct callers should do the same.
Retry & batching guidance
Section titled “Retry & batching guidance”The browser SDK implements the pattern below. Server callers wanting parity can mirror it:
- Batch up to 100 events per request, or flush every ~5 seconds, whichever comes first.
- Honor
Retry-Afteron429and5xx. - Exponential backoff with jitter for retries (e.g., starting at 1 s, capped around 30 s, with ±30% jitter).
- Stop retrying a batch on
400or413— the payload is malformed or too large; re-sending won’t help. Log it and drop. - Stop sending entirely on
401— the key is bad or revoked; surface this to your operators. - Retry on
429and5xxwith backoff. - Limit retry attempts (the SDK uses 5) so a sustained outage doesn’t queue events forever.
Examples
Section titled “Examples”Single feature.used
Section titled “Single feature.used”curl -X POST https://events.vektis.io/api/v1/events \ -H "Content-Type: application/json" \ -H "X-Vektis-Key: $VEKTIS_KEY" \ -d '{ "events": [{ "event_id": "550e8400-e29b-41d4-a716-446655440000", "event_type": "feature.used", "feature_id": "reports-dashboard", "customer_id": "acct_A1", "user_id": "user_123", "action": "opened", "properties": { "tab": "overview" } }] }'customer.identified
Section titled “customer.identified”curl -X POST https://events.vektis.io/api/v1/events \ -H "Content-Type: application/json" \ -H "X-Vektis-Key: $VEKTIS_KEY" \ -d '{ "events": [{ "event_id": "550e8400-e29b-41d4-a716-446655440001", "event_type": "customer.identified", "customer_id": "acct_A1", "user_id": "user_123" }] }'Batched (feature.used + feature.engagement)
Section titled “Batched (feature.used + feature.engagement)”curl -X POST https://events.vektis.io/api/v1/events \ -H "Content-Type: application/json" \ -H "X-Vektis-Key: $VEKTIS_KEY" \ -d '{ "events": [ { "event_id": "550e8400-e29b-41d4-a716-446655440010", "event_type": "feature.used", "feature_id": "reports-dashboard", "customer_id": "acct_A1", "user_id": "user_123", "action": "opened" }, { "event_id": "550e8400-e29b-41d4-a716-446655440011", "event_type": "feature.engagement", "feature_id": "reports-dashboard", "customer_id": "acct_A1", "user_id": "user_123", "action": "export", "properties": { "format": "csv", "row_count": 1240 } } ] }'