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:
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 totrack: identity and profile, no event.
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 anAuthorization 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.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.
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:type, if you sent it. Always wins.- A
value. Money moved, whatever you called the event. A negative amount is a refund. - A name Hamla already knows. Kept so that integrations written before
typeexisted keep behaving exactly as they did.
type says the same thing
in any vocabulary — but nothing you already send changes meaning because of
them.
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_case—businesses_count, notBusinesses-Count. - A field keeps its first type. Once
businesses_countis 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.