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 and context — replayed history carries no browser session and no live request, so nothing saw a User-Agent or a country.

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. It shows on the person’s timeline as identified, which is never a form, a lead or a conversion. Calling it on every page a signed-in person loads is safe.
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.

context — for signups that happen on your server

The browser SDK needs none of this: its request is the person, so Hamla reads the device and the country off it directly. A signup that completes on your server is the opposite — the request Hamla receives comes from your data centre. Without context that contact has no device and no country at all, and it is not a small gap: “which countries are my customers in” and “how many sign up on a phone” have no answer for anyone who signed in through an OAuth callback, a server-rendered form, or a mobile app. Read the values off the request the person actually made — user-agent, and whichever geo header your host adds (x-vercel-ip-country, cf-ipcountry):
Requires a secret key, for the same reason traits does: this is a claim about someone. From an unkeyed caller it is ignored with a warnings entry, and the request’s own headers are used instead. Send no context and Hamla records nothing rather than guessing — your hosting region is not where your customer lives. Geo is taken as country/region/city rather than as an IP address on purpose: Hamla keeps the place, never the address.

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: An email click Hamla records is read by where its link goes. A click toward your business (your site, product, booking page, app store, or a chat with you) is Interested. A click on a social profile counts as engagement and leaves the stage where it is. An unsubscribe click, or a mail scanner opening every link within seconds, moves no one. An email_clicked you send yourself always counts.

What an event means

event is your word. type is ours, and there are eleven 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 and a signed contract are 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.A free trial is not a booking: trying is not paying. trial_started is checkout_started — started, not finished — so a trial user is Interested, and becomes a Customer at their first payment.

What your ad accounts hear

When Google Ads or Meta is connected, Hamla tells them which results their ad clicks produced, by type and never by your event’s name: purchase (with the amount), booking, lead, signup, trial, contact and application each reach Google as their own conversion action and Meta as its standard event (Purchase, Schedule, Lead, CompleteRegistration, StartTrial, Contact, SubmitApplication). So treatment_completed sent with type: "purchase" reaches your ad account exactly like a store’s purchase. Imported history is never reported.

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_case — businesses_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.