Skip to content

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.

POST https://events.vektis.io/api/v1/events
Content-Type: application/json
X-Vektis-Key: vk_prd_...

Content-Type: application/json is required. Requests without it return 415 Unsupported Media Type.

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.

The body is always a events array, even for a single event:

{
"events": [ /* 1 to 100 TrackingEvent objects */ ]
}
ConstraintValue
Minimum events per request1
Maximum events per request100
Maximum body size512 KB
FieldRequiredTypeNotes
event_idyesstring (UUID)Caller-generated UUID. Generate this when the event happens, not on retry — see Idempotency.
event_typeyesenumOne of the five values in Event types. Other values are rejected.
customer_idyesstring, 1–255 charsTop-level on each event, not inside properties.
feature_idrequired for feature.* eventsstring, 1–255 charsfeature.used, feature.engagement, and feature.first_use all require this.
user_idnostring, ≤255 charsTop-level on each event, not inside properties.
actionnostring, ≤255 charsFree-form discriminator within an event_type (e.g., "started", "completed").
propertiesnoobjectSee constraints below.
timestampnoISO 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.
ConstraintValue
Maximum keys50
Maximum key length64 characters
Maximum string value length1024 characters
Maximum total JSON size8 KB
Allowed value typesstring, number, boolean

event_type is a fixed enum. Other values return 400 Bad Request.

event_typeNotes
feature.usedPrimary event — a customer used a feature. Requires feature_id.
feature.engagementEngagement signal within a feature (e.g., depth of use). Requires feature_id.
feature.first_useFirst-time-use signal for a customer/feature pair. Requires feature_id.
session.activeActive-session signal, used to anchor active-user metrics.
customer.identifiedAssociates a customer_id with optional user_id. Non-billable — does not count against your event quota.

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"
}]
}
{ "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).

All errors return JSON with at least statusCode and message:

{
"statusCode": 400,
"message": "Validation failed",
"errors": [ /* per-field issues */ ]
}
StatusWhenNotable body fields
400 Bad RequestSchema validation failed, or the body was not valid JSONerrors[] (per-field issues, present for schema failures)
401 UnauthorizedAPI key is missing or invalid
413 Payload Too LargeRequest body exceeded 512 KB
415 Unsupported Media TypeContent-Type was not application/json
429 Too Many RequestsRate limit exceededretryAfter (seconds); also returned as the Retry-After HTTP header
500 Internal Server ErrorUnhandled server error

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:

  1. accepted reflects queued events, not stored rows. If you re-send a batch after a network blip, the response can show accepted: 100 while none of those events are newly stored.
  2. Generate event_id when the event happens, not on retry. A fresh UUID per retry defeats dedupe; a stable UUID per event makes retries safe.

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.

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-After on 429 and 5xx.
  • Exponential backoff with jitter for retries (e.g., starting at 1 s, capped around 30 s, with ±30% jitter).
  • Stop retrying a batch on 400 or 413 — 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 429 and 5xx with backoff.
  • Limit retry attempts (the SDK uses 5) so a sustained outage doesn’t queue events forever.
Terminal window
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" }
}]
}'
Terminal window
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)”
Terminal window
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 }
}
]
}'