HTTP API
Everything the SDKs do goes through one endpoint, and you can use it directly from any language, a backend job or a device without an SDK. Messages follow the Segment spec.
Send events
curl -X POST https://ingest.heyglancy.com/api/ingest \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_WRITE_KEY" \
-d '{"batch": [
{"type": "identify", "userId": "user_42", "traits": {"name": "Ana Silva", "plan": "pro"}},
{"type": "track", "userId": "user_42", "event": "Order Completed", "properties": {"revenue": 42}}
]}'
The body can be a single message, an array of messages, or { "batch": [...] }.
Authentication
The bucket's write key (from its Setup page) decides where the events go. Send it in any one of these ways:
| How | Example |
|---|---|
| Bearer token (recommended) | Authorization: Bearer YOUR_WRITE_KEY |
| Basic auth, key as user name, empty password | Authorization: Basic base64("YOUR_WRITE_KEY:") |
| Header | x-write-key: YOUR_WRITE_KEY |
| Query parameter | https://ingest.heyglancy.com/api/ingest?key=YOUR_WRITE_KEY |
A write key can only add events. It can't read anything, so it's safe in browsers and apps. If one leaks and gets abused, a bucket admin can rotate it in Settings → Buckets.
Messages
Every message has a type and at least one of userId or anonymousId.
| Field | ||
|---|---|---|
type |
Required | track, page, screen, identify, group or alias |
userId |
One of the two | Your id for the person, once known |
anonymousId |
One of the two | An id for a visitor who hasn't signed in yet |
event |
track only |
The event name, e.g. Order Completed |
name |
page, screen |
The page or screen name |
properties |
Details of the event (track, page, screen) |
|
traits |
Details of the person (identify) or group (group) |
|
groupId |
group only |
The company, team or account id |
previousId |
alias only |
The id to merge into userId |
timestamp |
When it happened, ISO 8601. Defaults to when Hey Glancy received it | |
messageId |
A unique id per message. Hey Glancy keeps only one message per id, so retries are safe | |
context |
Device and page details: page, userAgent, ip, locale, timezone, os, device, app, screen, location, campaign… |
How each type is stored:
trackbecomes an event named afterevent.pagebecomes Page Viewed andscreenbecomes Screen Viewed, withnameand the properties.identifysaves the traits on the person's profile. If it carries both ananonymousIdand auserId, the anonymous history is merged into the user.groupties the person togroupId;aliasmergespreviousIdintouserId.
A full example
{
"type": "track",
"event": "Order Completed",
"userId": "user_42",
"anonymousId": "8b0c6d2e-4c2a-4f43-9b77-0f0d5a2a1e11",
"messageId": "order-1093",
"timestamp": "2026-10-21T14:03:12.000Z",
"properties": { "revenue": 49.9, "currency": "EUR", "items": 3 },
"context": {
"page": { "url": "https://shop.example.com/checkout?utm_source=newsletter", "path": "/checkout" },
"userAgent": "Mozilla/5.0 …",
"locale": "pt-PT",
"timezone": "Europe/Lisbon"
}
}
When context.page.url has UTM parameters or an ad click id, Hey Glancy fills context.campaign from it, so the
visit counts in ad attribution.
Response
{ "success": true, "bucket": "shop-production", "ingested": 2, "duplicates": 0, "bots": 0, "skipped": [] }
ingested: messages stored.duplicates: messages with amessageIdHey Glancy already had, ignored.bots: messages from crawlers, uptime monitors and headless browsers (bycontext.userAgent), dropped.skipped: messages that weren't valid, with their position in the batch and why, for example{ "index": 3, "reason": "userId or anonymousId is required" }or"track calls require an event name". The rest of the batch is still stored.
| Status | Meaning |
|---|---|
200 |
Accepted (check skipped for messages that weren't) |
400 |
No messages in the body, or it isn't JSON |
401 |
Missing or unknown write key |
413 |
The body is over 500 KB, or has more than 500 messages |
429 |
Rate limit reached; wait for the number of seconds in Retry-After |
Limits
- 500 KB per request and 500 messages per batch. A single message over 32 KB is skipped.
- 6,000 messages per minute per write key, and 300 requests per minute per IP address. Over a limit, the
answer is
429with aRetry-Afterheader; the SDKs keep the batch and try again.
Batch your messages: one request with 100 events is better than 100 requests.
Segment-compatible endpoints
Hey Glancy also answers Segment's HTTP Tracking API, so Segment libraries can send straight to it (see Segment):
POST https://ingest.heyglancy.com/v1/batch (also /v1/import)
POST https://ingest.heyglancy.com/v1/track
POST https://ingest.heyglancy.com/v1/page
POST https://ingest.heyglancy.com/v1/screen
POST https://ingest.heyglancy.com/v1/identify
POST https://ingest.heyglancy.com/v1/group
POST https://ingest.heyglancy.com/v1/alias
They take the same authentication and return the same response (without bucket). On the single-type endpoints,
the type comes from the path.
From the browser
/api/ingest allows cross-origin requests. To avoid a CORS preflight, send the body as text/plain with the key
in the query string, the way glancy.js does:
fetch("https://ingest.heyglancy.com/api/ingest?key=YOUR_WRITE_KEY", {
method: "POST",
headers: { "Content-Type": "text/plain" },
body: JSON.stringify({ type: "track", anonymousId, event: "Pricing Viewed" }),
keepalive: true,
});