How it works
The message_id is hashed and stored in a fast lookup table with a 24-hour TTL. Duplicates within that window return 202 Accepted but are not re-processed.
The Ingest API is the client-safe, high-volume API for tracking events, identifying contacts, and rendering widgets. Every endpoint is designed for low-latency ingestion from browsers, mobile apps, and edge workers.
| Base URL | https://ingest.growthos.io/v1 |
| Auth | Authorization: Bearer gos_wk_... |
| Content-Type | application/json |
All requests require a valid Write Key. Pass it via the Authorization header (recommended) or as a writeKey query parameter.
POST /v1/track HTTP/1.1Host: ingest.growthos.ioAuthorization: Bearer gos_wk_your_write_keyContent-Type: application/jsonIdentify or update a contact. Creates the contact if it does not exist (upsert). Traits are shallow-merged with existing data — set a trait to null explicitly to delete it.
{ "user_id": "usr_123", "traits": { "email": "jane@acme.com", "name": "Jane Doe", "plan": "growth", "company": "Acme Inc", "signed_up_at": "2025-01-15T10:00:00Z" }, "context": { "ip": "auto", "user_agent": "auto", "locale": "en-US" }, "timestamp": "2025-06-01T12:00:00Z"}| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | conditional | Your internal user identifier. Required if anonymous_id is not provided. |
anonymous_id | string | conditional | A UUID for anonymous visitors. Required if user_id is not provided. |
traits | object | no | Key-value pairs describing the contact. Shallow-merged with existing traits. Set a value to null to delete it. |
context | object | no | Contextual metadata. See Context Object below. |
timestamp | string | no | ISO 8601 timestamp. Defaults to server receipt time if omitted. |
HTTP/1.1 202 AcceptedContent-Type: application/json
{ "success": true, "request_id": "req_8x2m4k9p"}Track a custom event with optional properties.
{ "user_id": "usr_123", "event": "feature_activated", "properties": { "feature": "referral_widget", "plan": "growth", "value": 49.00 }, "timestamp": "2025-06-01T12:05:00Z", "message_id": "msg_abc123"}| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | conditional | Required if anonymous_id is not provided. |
anonymous_id | string | conditional | Required if user_id is not provided. |
event | string | yes | Event name. Max 256 characters. |
properties | object | no | Arbitrary key-value pairs describing the event. |
timestamp | string | no | ISO 8601 timestamp. Defaults to server receipt time. |
message_id | string | no | Client-generated unique ID for idempotent delivery. Deduped within 24 hours. |
context | object | no | Contextual metadata. See Context Object. |
HTTP/1.1 202 AcceptedContent-Type: application/json
{ "success": true, "request_id": "req_3n5p7q2z"}Track a page view. Automatically enriched with context fields when using the JavaScript SDK.
{ "user_id": "usr_123", "name": "Pricing Page", "properties": { "url": "https://acme.com/pricing", "referrer": "https://google.com", "title": "Pricing - Acme" }}| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | conditional | Required if anonymous_id is not provided. |
anonymous_id | string | conditional | Required if user_id is not provided. |
name | string | no | A human-readable name for the page (e.g., “Pricing Page”). |
properties | object | no | Page metadata — url, referrer, title, path, search, keywords. |
context | object | no | Contextual metadata. See Context Object. |
timestamp | string | no | ISO 8601 timestamp. Defaults to server receipt time. |
HTTP/1.1 202 AcceptedContent-Type: application/json
{ "success": true, "request_id": "req_6k8m2n4p"}Associate a contact with a company or organization. Use this to power account-level analytics and B2B features.
{ "user_id": "usr_123", "group_id": "company_456", "traits": { "name": "Acme Inc", "industry": "SaaS", "plan": "scale", "employee_count": 45 }}| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | conditional | Required if anonymous_id is not provided. |
anonymous_id | string | conditional | Required if user_id is not provided. |
group_id | string | yes | Your identifier for the company or organization. |
traits | object | no | Key-value pairs describing the group. Shallow-merged with existing traits. |
context | object | no | Contextual metadata. See Context Object. |
timestamp | string | no | ISO 8601 timestamp. Defaults to server receipt time. |
HTTP/1.1 202 AcceptedContent-Type: application/json
{ "success": true, "request_id": "req_1a3b5c7d"}Send multiple calls in a single HTTP request. Reduces connection overhead and improves throughput for high-volume producers.
{ "batch": [ { "type": "identify", "user_id": "usr_123", "traits": { "name": "Jane Doe" } }, { "type": "track", "user_id": "usr_123", "event": "login" }, { "type": "page", "user_id": "usr_123", "name": "Dashboard" } ]}| Field | Type | Required | Description |
|---|---|---|---|
batch | array | yes | Array of event objects. Each must include a type field. |
Each item in the batch array follows the same schema as the corresponding individual endpoint, with an additional type field.
type value | Corresponding endpoint |
|---|---|
identify | POST /v1/identify |
track | POST /v1/track |
page | POST /v1/page |
group | POST /v1/group |
| Constraint | Limit |
|---|---|
| Max events per batch | 500 |
| Max payload size | 500 KB |
Each item is processed independently — partial success is possible. The response includes per-item status.
HTTP/1.1 202 AcceptedContent-Type: application/json
{ "success": true, "request_id": "req_9z3n5p7q", "items": [ { "index": 0, "status": 202, "success": true }, { "index": 1, "status": 202, "success": true }, { "index": 2, "status": 422, "success": false, "error": "Missing required field: name" } ]}Merge two contact identities. Typically used to link an anonymous visitor to a known user after login or signup.
{ "previous_id": "anon_xyz789", "user_id": "usr_123"}| Field | Type | Required | Description |
|---|---|---|---|
previous_id | string | yes | The anonymous or old identifier to merge from. |
user_id | string | yes | The canonical user identifier to merge into. |
previous_id are transferred to user_idprevious_id becomes a permanent alias pointing to user_idprevious_id are automatically attributed to user_idHTTP/1.1 202 AcceptedContent-Type: application/json
{ "success": true, "request_id": "req_2b4d6f8h"}Retrieve active feature flags, experiment variants, and widget configurations for a specific contact. This is the only Ingest API endpoint that returns data.
GET /v1/decide?user_id=usr_123| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | conditional | Required if anonymous_id is not provided. |
anonymous_id | string | conditional | Required if user_id is not provided. |
HTTP/1.1 200 OKContent-Type: application/json
{ "feature_flags": { "new_onboarding": true, "dark_mode": false }, "active_experiments": [ { "key": "pricing_test", "variant": "B" } ], "widgets": { "referral": { "enabled": true, "config": { "reward_type": "credit", "reward_amount": 20, "currency": "USD" } }, "nps": { "enabled": true, "delay_ms": 30000 } }}Every Ingest API call accepts an optional context object for metadata enrichment. When using the JavaScript SDK, most fields are populated automatically.
| Field | Type | Auto-populated | Description |
|---|---|---|---|
ip | string | yes | Client IP address. Set to "auto" to use the request IP. |
user_agent | string | yes | Browser or client user agent string. Set to "auto" for automatic detection. |
locale | string | yes | User locale (e.g., en-US). |
page.url | string | yes | Full URL of the current page. |
page.path | string | yes | URL path (e.g., /pricing). |
page.referrer | string | yes | Referring URL. |
page.title | string | yes | Document title. |
screen.width | integer | yes | Screen width in pixels. |
screen.height | integer | yes | Screen height in pixels. |
library.name | string | yes | SDK name (e.g., growthos-js). |
library.version | string | yes | SDK version (e.g., 1.4.2). |
campaign.source | string | no | UTM source parameter. |
campaign.medium | string | no | UTM medium parameter. |
campaign.name | string | no | UTM campaign name. |
campaign.term | string | no | UTM term. |
campaign.content | string | no | UTM content. |
Events prefixed with $ are reserved by GrowthOS and have special handling in the platform. Do not use these prefixes for custom events.
| Event Name | Description | Triggered By |
|---|---|---|
$page_view | Page view recorded | SDK auto-track or /v1/page |
$form_submitted | A GrowthOS-managed form was submitted | Web Components |
$referral_created | A referral link was generated | Referral widget |
$referral_converted | A referred user completed signup | Referral engine |
$survey_responded | A user submitted a survey response | Survey widget |
$waitlist_joined | A user joined a waitlist | Waitlist widget |
$email_opened | An email was opened (pixel tracked) | Email engine |
$email_clicked | A link in an email was clicked | Email engine |
$email_bounced | An email delivery bounced | Email engine |
$email_unsubscribed | A user unsubscribed from emails | Email engine |
$experiment_viewed | An experiment variant was displayed | A/B testing engine |
$nps_submitted | An NPS score was submitted | NPS widget |
Include a message_id in any event payload to enable idempotent delivery. If GrowthOS receives two events with the same message_id within a 24-hour window, the duplicate is silently discarded.
{ "user_id": "usr_123", "event": "purchase_completed", "properties": { "order_id": "ord_789", "amount": 99.00 }, "message_id": "msg_unique_abc123"}How it works
The message_id is hashed and stored in a fast lookup table with a 24-hour TTL. Duplicates within that window return 202 Accepted but are not re-processed.
When to use it
Always include message_id when retrying failed requests or sending from at-least-once delivery systems (e.g., message queues). The SDK generates one automatically for every call.
All Ingest API write endpoints return 202 Accepted immediately after validating the payload structure. Events are placed on an internal event bus and processed asynchronously.
| Aspect | Guarantee |
|---|---|
| Acknowledgment | 202 returned in under 50ms (p99) |
| Processing latency | Events processed within 5 seconds (p95) |
| Durability | Events are persisted to the event bus before 202 is returned |
| Ordering | Per-user ordering is preserved. Cross-user ordering is best-effort. |
| Delivery | At-least-once. Use message_id for exactly-once semantics. |
Even though successful calls return 202, the Ingest API validates payloads synchronously and returns errors immediately.
| Status | Code | Description |
|---|---|---|
400 | bad_request | Malformed JSON or missing Content-Type header. |
401 | unauthenticated | Missing or invalid Write Key. |
413 | payload_too_large | Batch payload exceeds 500 KB. |
422 | validation_error | Valid JSON but fails schema validation (e.g., missing event field). |
429 | rate_limited | Too many requests. Check the Retry-After header. |
{ "error": { "code": "validation_error", "message": "Field 'event' is required for track calls.", "details": [ { "field": "event", "reason": "required", "message": "Every track call must include an event name." } ] }, "request_id": "req_4f6h8j0l"}POST /v1/identify
Upsert a contact with traits. Requires user_id or anonymous_id. Returns 202.
POST /v1/track
Record a custom event. Requires event name. Supports message_id for idempotency. Returns 202.
POST /v1/page
Log a page view with URL, referrer, and title metadata. Returns 202.
POST /v1/group
Associate a contact with a company. Requires group_id. Returns 202.
POST /v1/batch
Send up to 500 events in one request. Max 500 KB. Per-item status in response.
POST /v1/alias
Merge anonymous identity into known user. Irreversible. Returns 202.
GET /v1/decide
Fetch feature flags, experiments, and widget config. Synchronous 200 response.