Skip to main content
An event is a fact your system reports to Hamla: something happened, to a specific person, at a specific time. Once stored it can start a campaign, define an audience, end a sequence, and move the contact through their journey. For the guided version, read Hamla Events. This page is the contract.

The doors

Three carry events. The fourth carries the person they happened to. Passive autocapture — page views, scrolls, idle timers gathered by the script tag on its own — is another stream. It feeds analytics and behavioral triggers, but it never opens an event trigger. Deliberateness is never guessed from the event name.

Authentication

track and track/batch require a secret key:
Create one in Settings → API keys. Older hamla_live_… keys keep working. A publishable key (pk_live_…) authenticates but is refused with 403 on these endpoints: it ships in page source, so honoring it would let any visitor invent revenue. Neither endpoint sends CORS headers or answers OPTIONS, deliberately — a page that cannot pass CORS cannot be tempted to embed a secret key. The key names exactly one business. businessId in the body is optional; if present and it disagrees with the key, the request is refused rather than quietly rewritten.

POST /api/sdk/track

Records one event that just happened.

Fields

identity

Any one is enough, and it does not have to be the email — a business that only ever collects phone numbers sends phone alone, forever. An event with no identity at all belongs to nobody and is refused. Send more than one and Hamla uses them together to match or create a single contact, keeping each as a way to find that person later. Anonymous browsing history joins the profile at the moment identity is first known. Identity is recognition, not permission: a phone number lets Hamla match someone, and never doubles as consent to send them an SMS or WhatsApp message.

Response

Errors

POST /api/sdk/track/batch

Records up to 500 events that already happened. Same event vocabulary, same field rules, same secret key.
events takes 1–500 items. Each item accepts every track field except visitorId — replayed history carries no browser session.

What backfill deliberately does not do

Everything a backfill should do still happens: contacts are resolved or created, activities land with their real occurredAt, first/last/count statistics update (out-of-order rows are handled), traits and tags apply, and value counts toward revenue.

Response

Items fail alone — a bad row reports its index and reason, and the rest of the batch lands.
warnings is capped at 50 entries so one misnamed column across 500 rows cannot dominate the response. Give every row an idempotencyKey and the whole import is safe to re-run.

POST /api/sdk/identify

Says who someone is — and what to call them. The companion to track: identity and profile, no event.
Resolve-or-create: an unknown person becomes a contact, a known one is enriched. Safe to call on every login — profile fields are gap-filled, never overwritten.

Fields

profile

Worth sending a name for one reason: {{contact.firstName}} in a campaign. Without it every message opens with Hi Friend.

Authentication, unusually

This is the one write endpoint that also serves unkeyed callers, because published browser bundles cannot send an Authorization header and cannot be upgraded once a customer’s CDN has them. A key is still worth sending from a server: it scopes the write to your business and makes it attributable, and traits require one.

Response

linkedVisitor is true when this call is what tied the anonymous browser to the person — the moment their earlier visits, and the campaign that produced them, stop being anonymous. warnings is absent when everything landed.

hamla.track() — the browser door

For deliberate actions that genuinely happen in the page. No key, because none can safely ship to a browser.
The event name is used verbatim. Because the page’s own code chose to send it, it counts as deliberate and can start a campaign — unlike the autocapture events travelling in the same batch. Anything involving money or your own records belongs on the server door, where the key lives and the facts cannot be edited by a visitor.

What an event does once stored

Segment fields

A contact with no matching event resolves to undefined for all metrics, including count. So “fewer than 5 times, including never” is count < 5 OR not_exists — never a silent zero.

Names that move the journey stage

Your own names are recorded and fully usable; they simply do not move the stage. Borrow these when they fit:

What an event means

event is your word. type is ours, and there are seven of them. You never have to send type — Hamla infers from the amount and from names it already recognises. Send it and nothing is guessed.
A clinic calls it treatment_completed, a school calls it lesson_finished, and both are a purchase. Your name is never translated, replaced or checked against a list — it is kept exactly as you sent it, and stays what your reports, segments and triggers are written in.
booking is the one worth knowing about. Every other analytics vocabulary was designed for online stores, where saying yes is paying. A confirmed appointment, a signed contract and a free trial are all real commitments that move no money — so they are a booking, not a purchase, and they will not show up in revenue or inflate your average order value. When one of them does carry an amount, send the amount and Hamla counts it.

If you send nothing

Hamla decides in this order:
  1. type, if you sent it. Always wins.
  2. A value. Money moved, whatever you called the event. A negative amount is a refund.
  3. A name Hamla already knows. Kept so that integrations written before type existed keep behaving exactly as they did.
If none apply, the event is still recorded, still segmentable and can still start a campaign — it just does not claim to be revenue. These are the names in full. You never need them — type says the same thing in any vocabulary — but nothing you already send changes meaning because of them.
An amount is never counted silently. If you send a value and Hamla cannot tell what it means, the response says so in warnings rather than storing it and moving on. Every response also echoes interpreted, so you can see the decision on your first request instead of in a report weeks later.

Traits and custom fields

traits writes the contact’s current state; the event records what happened. Send both in one call. The first time a trait key arrives, Hamla creates the matching custom field and infers its type: 3 becomes Number, true Boolean, "2026-08-08" Date, anything else Text. It shows up in Contacts → Contacts Management → Custom Fields and segments can filter on it immediately. Two rules, both reported in the warnings array:
  • Keys are snake_casebusinesses_count, not Businesses-Count.
  • A field keeps its first type. Once businesses_count is a Number, a later "lots" is skipped rather than corrupting what is stored.

Limits

The rate limiter fails open: if it is unreachable, requests are allowed rather than rejected.

Node SDK

npm install @gethamla/node wraps the same endpoints — nothing it can do is unavailable over plain HTTP.
track and identify are fire-and-forget and never throw — a marketing call must not break a checkout. trackNow returns a DeliveryResult when you want to know. trackBatch re-bases failure indexes onto your own array, and refuses visitorId for the same reason the endpoint does.