SDK ingest
Send ordered events, identities, attributes and devices from a custom or server-side client with POST /sdk/batch.
The official SDKs call these endpoints for you. Use them directly only if you are writing your own client, for example for a platform without an SDK. For servers, prefer POST /events.
These endpoints return their payload directly, without the usual envelope.
Authentication
Send the SDK app key in the X-TE-App-Key header. It needs no secret. The key decides the environment, so a test_ key can never write Live data. A browser sendBeacon call cannot set headers, so POST /sdk/batch also accepts ?appKey=test_xxxxxxxx.
Optional headers on /sdk/batch:
| Header (or query) | Meaning |
|---|---|
X-TE-Batch-Id (batchId) |
An id for the whole batch, used to ignore a repeated batch. Use a UUID v7. |
X-TE-Sdk (sdk) |
Your client name and version, such as mycustomclient/1.0.0. |
POST /sdk/batch
POST https://api.true-engage.com/v1/sdk/batch
Scope sdk.track. The server processes items in array order, so an identify placed before an event applies to it.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
sentAt |
string (ISO 8601) | Yes | When your client flushed. Used to correct device clock skew. |
context |
object | Yes | Who and what is sending. See below. |
items |
array | Yes | Up to 500 items. |
context
| Field | Notes |
|---|---|
anonymousId |
Required. A UUID your client generates and keeps until reset or uninstall. Up to 128 characters. |
externalId |
Set after identify. |
deviceUid |
A UUID per install. |
sessionId |
Absent for non-interactive events. |
sdkVersion, sdkPlatform, appVersion, appBuild, bundleId |
App and SDK details. |
osName, osVersion, deviceModel, deviceManufacturer |
Device details. |
screenWidth, screenHeight, screenDensity |
Non-negative integers. |
networkType |
wifi, cellular, none or unknown. The web SDK reports wifi for wired and Wi-Fi connections, because a browser cannot tell them apart. Only Chromium on Android reports a connection type. Safari, Firefox and most desktop browsers send unknown. none means the browser is offline. |
carrier, locale, timezone |
Strings. |
deviceCountry |
ISO 3166-1 alpha-2 code. |
deviceLatitude, deviceLongitude, deviceAccuracyMetres, deviceCity, deviceRegion, deviceLocationFixAt, deviceLocationPermission |
Optional location, only after the user has granted permission. |
isFirstSession |
Boolean. |
utmSource, utmMedium, utmCampaign, utmTerm, utmContent, utmId |
Campaign attribution. |
advertisingId |
Only with the user's opt-in. |
Items
Every item has:
| Field | Required | Notes |
|---|---|---|
type |
Yes | event, identify, attributes, device or alias. |
messageId |
Yes | A unique id per item, up to 64 characters (UUID v7 recommended). Resend the same id when you retry. |
timestamp |
Yes | ISO 8601, when it happened on the device. |
Fields by type:
| Type | Fields |
|---|---|
event |
name (required, up to 100 characters), properties (up to 50 keys), nonInteractive (boolean; does not start or extend a session) |
identify |
externalId (up to 256 characters), attributes (object) |
attributes |
set (object of changed keys), unset (array of keys) |
device |
platform (android, ios or web), pushProvider (fcm, apns, web_push or hms), pushToken (up to 4,096 characters), apnsEnvironment (production or sandbox, iOS only), notificationsEnabled (boolean), webPushSubscription (object) |
alias |
Accepted as a type. |
Re-send the device item on every app launch. Token-change callbacks are unreliable, and a repeat is free.
Example
curl -X POST https://api.true-engage.com/v1/sdk/batch \
-H "X-TE-App-Key: test_xxxxxxxx" \
-H "X-TE-Batch-Id: 01J9Z3K8M2Q7X4N5P6R7S8T9V0" \
-H "Content-Type: application/json" \
-d '{
"sentAt": "2026-10-18T09:30:05Z",
"context": {
"anonymousId": "7b1d6f0a-3c52-4c0e-9d2a-5f6e7a8b9c0d",
"deviceUid": "c3e4f5a6-1b2c-4d3e-8f4a-5b6c7d8e9f01",
"sdkPlatform": "custom",
"sdkVersion": "1.0.0"
},
"items": [
{
"type": "identify",
"messageId": "01J9Z3K8M2Q7X4N5P6R7S8T9V1",
"timestamp": "2026-10-18T09:30:00Z",
"externalId": "u-1001",
"attributes": { "firstName": "Pritam" }
},
{
"type": "event",
"messageId": "01J9Z3K8M2Q7X4N5P6R7S8T9V2",
"timestamp": "2026-10-18T09:30:02Z",
"name": "Order Placed",
"properties": { "orderId": "ORD-8891", "amount": 2499.5 }
}
]
}'
const res = await fetch('https://api.true-engage.com/v1/sdk/batch', {
method: 'POST',
headers: { 'X-TE-App-Key': 'test_xxxxxxxx', 'Content-Type': 'application/json' },
body: JSON.stringify({
sentAt: new Date().toISOString(),
context: { anonymousId, deviceUid, sdkPlatform: 'custom', sdkVersion: '1.0.0' },
items,
}),
});
if (res.status === 413) { /* split items in half and send both halves */ }
else if (res.status === 202 || res.status === 207) {
const { accepted, rejected, errors } = await res.json();
}
Responses
| Status | Meaning | What to do |
|---|---|---|
| 202 | Every item was accepted and queued. | Remove the items from your queue. |
| 207 | Some items were rejected for good. errors lists each by messageId. |
Remove the whole batch. Never retry rejected items; they would fail again. |
| 400 | The envelope is malformed. | Drop the batch. Retrying never succeeds. |
| 413 | The batch is too large. | Split it in half and send both halves. |
| 429 | Rate limited. | Wait for Retry-After, then retry. |
| 5xx or network error | Temporary. | Keep the items and retry with backoff, with the same messageIds. |
{
"accepted": 1,
"rejected": 1,
"batchId": "01J9Z3K8M2Q7X4N5P6R7S8T9V0",
"errors": [
{ "messageId": "01J9Z3K8M2Q7X4N5P6R7S8T9V2", "code": "EVENT_BLOCKED", "message": "That event is blocked for this workspace." }
]
}
Limits
| Limit | Value |
|---|---|
| Items per batch | 500 |
| Batch size | 1 MiB (1,048,576 bytes) |
| Event name | 100 characters |
| Properties per event | 50 |
| Property key | 100 characters |
| String value | 1,000 characters |
| Array elements | 100 |
| Event payload | 32 KB |
| Event time | Not more than 48 hours ahead, or 30 days behind |
| Rate | 6,000/min per key, 300/min per IP |
Accepted means queued. The write happens shortly after, so a profile can take a moment to show the new event.
GET /sdk/config
Remote configuration for your client.
curl https://api.true-engage.com/v1/sdk/config \
-H "X-TE-App-Key: test_xxxxxxxx" \
-H 'If-None-Match: "abc123"'
The response carries an ETag header and Cache-Control: max-age=3600. Send the last ETag in If-None-Match. If nothing changed you get 304 Not Modified with no body. Cache the last config to disk and use it on the next launch, even offline. Limit: 600/min per key, 60/min per IP.
Fields in the body:
| Field | Meaning |
|---|---|
version |
Config version number. |
reservedEvents, reservedAttributes |
Names your client must not send. |
blockedEvents, blockedAttributes |
Names to drop before queueing. This is the kill switch for a bad event. |
flush |
intervalSeconds, batchSize, maxQueueSize. |
session |
timeoutMinutes. |
features |
Feature flags. |
sampling |
eventSampleRates, per event name. |
endpoints |
Endpoint URLs for the client. |
Other SDK endpoints
| Endpoint | Scope | Purpose |
|---|---|---|
POST /sdk/track, /sdk/identify, /sdk/devices |
sdk.track, sdk.identify, sdk.device |
Aliases that accept the same body as /sdk/batch. Use /sdk/batch to keep ordering. |
GET /sdk/inbound |
sdk.inbound |
Fetch in-app, on-site and card messages. Query: types, screen, anonymousId, externalId. Supports ETag. |
POST /sdk/inbound/stats |
sdk.inbound |
Report impressions, up to 500 per call. |
POST /sdk/links/resolve, /sdk/links/deferred |
sdk.track |
Called by the SDKs to resolve a smart link the app was opened with, and to match a first launch to a link click. Use the SDK rather than calling these. See Deep links and deferred deep links. |
POST /sdk/links/create |
sdk.track |
Creates a short link for in-app content. See Links API and webhooks. |
Errors
See Errors for every code, and Authentication for key problems.
Was this page helpful?