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:

  • track becomes an event named after event.
  • page becomes Page Viewed and screen becomes Screen Viewed, with name and the properties.
  • identify saves the traits on the person's profile. If it carries both an anonymousId and a userId, the anonymous history is merged into the user.
  • group ties the person to groupId; alias merges previousId into userId.

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 a messageId Hey Glancy already had, ignored.
  • bots: messages from crawlers, uptime monitors and headless browsers (by context.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 429 with a Retry-After header; 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,
});