Event ingestion
The gateway exposes several SDK-authenticated paths, all write-key
authed via the X-Aegis-Write-Key header (no JWT). The three core
ingestion streams:
| Endpoint | Carries | Payload type |
|---|---|---|
POST /v1/batch | SDK event stream — track / identify / page / screen / group / alias | SDKBatchPayload |
POST /v1/in_app/events | In-app campaign click + dismiss telemetry (impression / clicked / dismissed) | InAppEventPayload |
POST /v1/push/engagement | Push notification lifecycle from mobile + web push SDKs (delivered / shown / clicked / dismissed / permission_denied / subscribed / resubscribed) | PushEngagementEventPayload |
Plus the device-token + in-app campaign control paths the mobile SDKs
use — device registration (/v1/devices/register,
/v1/devices/deactivate) and in-app fetch + feedback
(GET /v1/in-app/active, /v1/in-app/widgets/{step}/ack,
/api/v1/in_app/responses) — documented below.
All terminate at the active-reach-gateway and dispatch into the
tenant’s cell. Use /v1/batch for application events; the other
endpoints have narrowly-scoped payload contracts and exist so the
high-volume in-app + push telemetry streams can be rate-limited and
routed independently of the main event stream.
Geofence enter/exit are not a separate endpoint — the mobile SDKs emit
geofence.entered / geofence.exited as ordinary track events through
/v1/batch. See the iOS,
Android,
React Native, and
Flutter geofencing guides.
/v1/batch — SDK event stream
POST https://ingest.active-reach.ai/v1/batch
X-Aegis-Write-Key: <sdk_write_key>
Content-Type: application/jsonRequest body:
{
"batch": [
{
"type": "track",
"event": "order_completed",
"messageId": "msg_01HXXXXX",
"timestamp": "2026-05-27T10:30:00.000Z",
"anonymousId": "anon_789",
"userId": "user_456",
"sessionId": "sess_abc",
"properties": {
"order_id": "ORD-123",
"amount": 4999,
"currency": "INR"
}
}
],
"sentAt": "2026-05-27T10:30:01.000Z",
"writeKey": "sdk_write_YOUR_KEY"
}The writeKey field in the body is the authoritative auth signal; the
Web SDK also sets Authorization: Bearer <writeKey> for backward
compatibility. The gateway rejects with 401 on a missing or invalid
write key.
Top-level fields
| Field | Required | Description |
|---|---|---|
batch | Yes | Array of one or more event records (see below) |
sentAt | Yes | ISO 8601 timestamp — when the SDK flushed this batch |
writeKey | Yes | The workspace SDK write key |
context | No | Cell metadata stamped by the SDK transport (region, endpoint) |
Per-event fields
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | One of: track, identify, page, screen, group, alias |
event | string | Conditional | Event name. Required for track. |
name | string | Conditional | Page/screen name for page / screen. |
messageId | string | Yes | SDK-generated unique id for dedup |
timestamp | ISO 8601 | Yes | When the event occurred (SDK clock) |
anonymousId | string | Yes | SDK-generated anonymous id |
userId | string | No | Your application user id (when identified) |
sessionId | string | Yes | SDK-managed session id |
properties | object | No | Event-specific payload |
traits | object | No | Identity traits (for identify / group) |
context | object | No | Page / device / campaign metadata |
/v1/in_app/events — in-app campaign telemetry
POST https://ingest.active-reach.ai/v1/in_app/events
X-Aegis-Write-Key: <sdk_write_key>
X-Organization-ID: <org_id>
Content-Type: application/json{
"campaign_id": "iac_abc123",
"event_type": "clicked",
"user_id": "user_456",
"anonymous_id": "anon_789",
"platform": "web",
"workspace_id": "ws_abc123",
"idempotency_key": "iac_abc123:user_456:clicked"
}event_type is one of impression / clicked / dismissed. This
endpoint is narrowly scoped to in-app campaign feedback — application
events go to /v1/batch.
/v1/push/engagement — push notification lifecycle
POST https://ingest.active-reach.ai/v1/push/engagement
X-Aegis-Write-Key: <sdk_write_key>
X-Organization-ID: <org_id>
X-Property-Id: <property_id>
Content-Type: application/json{
"event_type": "push.clicked",
"platform": "ios",
"campaign_id": "cmp_42",
"message_id": "msg_a1b2",
"property_id": "prop_x",
"contact_id": "ct_z9",
"anonymous_id": "an_q4",
"metadata": {
"action_url": "myapp://product/sku-42",
"action_id": "primary"
}
}Every Active Reach SDK (Web, Android, iOS, React Native, Flutter)
posts to this endpoint when a push notification is delivered, shown,
clicked, or dismissed by the operating system. The gateway forwards
to the event switchboard, which dual-routes onto the workspace’s
delivery_events ClickHouse table as channel='push' and into
timeline_cards for the activity feed.
Allowed event_type values
| Value | Fires when | Delivery status mapping |
|---|---|---|
push.delivered | OS confirmed the notification reached the device | shown |
push.shown | Banner rendered (web push) | shown |
push.clicked | User tapped the notification | clicked |
push.dismissed | User swiped the notification away | dismissed |
push.permission_denied | User declined the push permission prompt | (no delivery row) |
push.subscribed | New push subscription minted | (no delivery row) |
push.resubscribed | Existing subscription refreshed (token rotation) | (no delivery row) |
The gateway tolerates bare delivered / clicked / dismissed event
types and normalises them to the push.<event> prefix before
dispatch — legacy SDK versions can interop while you upgrade.
Required fields
| Field | Type | Required | Description |
|---|---|---|---|
event_type | string | Yes | One of the values above |
platform | string | Yes | web / ios / android |
property_id | string | Yes | The Active Reach property the SDK install belongs to |
campaign_id | string | No | Empty string when the SDK cannot resolve (e.g. a transactional push) |
message_id | string | No | Same — empty string when not resolvable |
user_id | string | No | Falls back to contact_id when absent |
contact_id | string | No | Active Reach contact id (when identified) |
anonymous_id | string | No | SDK-generated anonymous id |
metadata | object | No | Arbitrary key/value — action_url, action_id, button_id, etc. |
The endpoint is best-effort. Push service workers (web), Notification Service Extensions (iOS), and FCM background callbacks (Android) never propagate a failed engagement POST — the OS has already displayed the notification by the time the SDK tries to report.
Push engagement vs delivery webhooks
The /v1/push/engagement endpoint is inbound ingestion from your
SDK. Push engagement also flows outbound to your webhook endpoint
as standard delivery.delivered / delivery.opened / delivery.clicked
events — see Webhook event types.
You do not need to consume both: the ingestion endpoint is for the SDK,
the webhook is for your downstream systems.
Device registration
Mobile SDKs register their APNs / FCM device token so the platform can target the install with push. Two endpoints manage the token lifecycle.
POST /v1/devices/register
POST https://ingest.active-reach.ai/v1/devices/register
X-Aegis-Write-Key: <sdk_write_key>
X-Organization-ID: <org_id>
Content-Type: application/json{
"device_token": "<apns_or_fcm_token>",
"platform": "ios",
"property_id": "prop_x",
"contact_id": "ct_z9",
"anonymous_id": "an_q4",
"app_version": "1.4.0",
"user_agent": "ActiveReach-iOS-SDK/1.6.0 iOS 17.4 (iPhone)"
}Called by the SDK after the OS grants push permission and mints a token,
and again on every token rotation. Re-registering the same token is
idempotent — the platform upserts on (property_id, device_token).
| Field | Type | Required | Description |
|---|---|---|---|
device_token | string | Yes | Raw APNs hex token or FCM registration token |
platform | string | Yes | ios / android |
property_id | string | No | The Active Reach property the install belongs to |
contact_id | string | No | Active Reach contact id (when identified) |
anonymous_id | string | No | SDK-generated anonymous id |
app_version | string | No | Host app version, for cohorting |
user_agent | string | No | SDK + OS descriptor |
POST /v1/devices/deactivate
POST https://ingest.active-reach.ai/v1/devices/deactivate
X-Aegis-Write-Key: <sdk_write_key>
Content-Type: application/json{
"device_token": "<apns_or_fcm_token>",
"platform": "ios",
"property_id": "prop_x",
"reason": "apns_inactive"
}Called when the push service reports a token as inactive (APNs feedback
on iOS, an UNREGISTERED / SenderId mismatch from FCM on Android). The
platform marks the token inactive with a soft-quarantine window so a
device that comes back online can re-register without losing history.
reason is a free-form diagnostic string (e.g. apns_inactive,
fcm_unregistered).
In-app campaign fetch + feedback
In-app messaging uses one canonical fetch endpoint plus two feedback paths. The fetch returns the campaigns eligible for the current install (catalog campaigns + journey-queued widgets, consent- and frequency-gated). Feedback splits by campaign origin — see the table below.
GET /v1/in-app/active
GET https://ingest.active-reach.ai/v1/in-app/active?current_surface=<surface>
X-Aegis-Write-Key: <sdk_write_key>
X-Organization-ID: <org_id>
X-Contact-ID: <contact_id>
X-User-ID: <user_id>Returns a JSON array of eligible in-app campaigns. current_surface is
optional — when present (one of the canonical placement surfaces, e.g.
bill, storefront_catalog), the platform drops campaigns scoped to
other surfaces; when absent, no surface filter is applied. This is the
single fetch endpoint for every SDK (web + the four mobile SDKs).
POST /v1/in-app/widgets/{step_execution_id}/ack
Acknowledges a journey-queued in-app widget (one that carries a
journey_step_execution_id). Used for served / viewed / clicked /
dismissed on journey-driven in-apps so the journey can advance.
{ "action": "clicked", "step_id": "node_3", "variant_id": "v_b" }action is required (served / viewed / clicked / dismissed);
step_id and variant_id are optional (multi-step renderers + A/B
attribution).
POST /api/v1/in_app/responses
Submits a structured response from an interactive in-app widget (NPS, poll, quiz, rating, multi-step form, countdown).
{
"campaign_id": "iac_abc123",
"response_type": "nps",
"platform": "ios",
"payload": { "score": 9 },
"user_id": "user_456",
"contact_id": "ct_z9",
"variant_id": "v_b"
}| Field | Type | Required | Description |
|---|---|---|---|
campaign_id | string | Yes | The in-app campaign id |
response_type | string | Yes | Widget type — nps / poll / quiz / rating / form / countdown |
platform | string | Yes | web / ios / android |
payload | object | Yes | Response data (shape depends on response_type) |
user_id | string | No | Application user id |
contact_id | string | No | Active Reach contact id |
variant_id | string | No | A/B variant id, when assigned |
Which feedback path?
| Campaign origin | Carries journey_step_execution_id? | Click / dismiss path | Structured response path |
|---|---|---|---|
Catalog (operator-authored, served by /v1/in-app/active) | No | POST /v1/in_app/events | POST /api/v1/in_app/responses |
| Journey-queued (a journey SEND step) | Yes | POST /v1/in-app/widgets/{step}/ack | POST /api/v1/in_app/responses |
The SDK routes automatically based on whether the campaign payload
carries a journey_step_execution_id — you don’t choose the path by
hand.
Batch limits
| Limit | Value |
|---|---|
Max events per batch (/v1/batch) | 100 |
| Max payload size | 500 KB |
| Max property depth | 5 levels of nesting |
| Max property key length | 256 characters |
| Max property value size | 10 KB per value |
Authentication
X-Aegis-Write-Key: sdk_write_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxWrite keys are scoped to a single workspace. Manage them under Settings → Developer → SDK Keys. Per-credential rate limits and an origin allow-list are configured alongside the key.
Response
| Status | Meaning |
|---|---|
200 | All events accepted |
400 | Missing writeKey / empty batch / malformed JSON |
401 | Invalid or missing write key |
429 | Rate limited — respect Retry-After and back off |
502 | Cell unavailable for tenant |
Rate limits
The gateway enforces per-tenant ingress limits plus optional
per-write-key minute + hour caps. 429 responses include
Retry-After and X-Aegis-Rate-Reset headers in seconds.
Server-side tracking tips
- Always set
timestamp— server receive time is inaccurate for batch imports - Use
userIdoveranonymousIdwhen you know the user — it enables identity resolution - Batch aggressively — 50-100 events per request is optimal
- Retry on
5xxand429with exponential backoff
What’s next
- Event schema — the six event types in detail
- Receipt events — delivery status events from channels
- MAU classification — how events affect billing