Overview
The Contact Capture API turns any form submission into a Contact — a first-class person identity that exists before (and independently of) a logged-in user account. Every submission is deduplicated, enriched, and emits events that flow into segments, automations, inbox, campaigns, analytics, and AI.
Base URL:
https://api.hacktionbase.com/v1
Both endpoints are asynchronous: the submission is validated at the edge, accepted with 202, and queued. Deduplication, identity resolution, and event emission happen downstream within seconds.
Authentication
Both endpoints require a public API key (hb_public_*), sent as a Bearer token:
Upsert a contact
POST /contacts accepts a form submission and either creates a new contact or updates the matching one.
Request fields
At least one identifier (
externalUserId, email, phone, or anonymousId) is required.
Limits: string fields are capped at 2048 characters; traits is capped at 100 keys; keys containing $ or . are silently dropped.
Provenance is captured automatically on every submission: ip, userAgent, and receivedAt are stored in the contact’s sources[] array along with source, formId, and formType.
Response
202 Accepted — the submission is queued for processing:
identityStatus: "conflict" with the candidate ids in conflictsWith — and a contact.identity_conflict event is emitted. Conflicts are never auto-merged; resolve them from the dashboard (or by assigning the contact to a user).
Identify a contact
POST /contacts/identify resolves an existing contact and marks it identified — it never creates one. Use it when identity information arrives after the initial capture (e.g. the visitor signs up).
202 Accepted acknowledgment. Downstream:
- Match found — the contact is enriched and marked identified (
contact.identifiedemitted). - No match — the submission is dropped (identify never creates; a later re-submission will re-identify).
- Ambiguous match — nothing is merged; a
contact.identity_conflictevent is emitted and the conflict surfaces in the dashboard.
Identity resolution
Matching walks the identifiers in priority order over all active contacts (leads, anonymous visitors, and users):externalUserIdemail(normalized)phone(E.164 only)anonymousId
Events
Every processed submission generates at least one event — there is no submission without an event. Events are written to the tenant event store during processing, then fanned out to the event bus (automations, segments, inbox, analytics, messaging, AI). System events:contact.created, contact.updated, contact.form_submitted, contact.identified, contact.merged, contact.identity_conflict.
Business events: pass any name in the event field (e.g. quote.requested, newsletter.subscribed, demo.requested). Business events are dynamic — consolidate and map them per tenant in Analytics → Discovery with the Map Events modal.
Server-side proxy pattern
Keep the key on your server and forward form submissions from your own endpoint:anonymousId) so the form contact links to the browsing session. The SDK exposes it via the session — the same id the widget uses for Hacktionbase.identify().
Error reference
Only edge validation errors are returned synchronously:
Business outcomes are resolved downstream and never fail the request: a submission with no identifier, an identify with no match, an exhausted trial contact quota, or a frozen trial tenant is dropped (logged server-side). Ambiguous matches emit a
contact.identity_conflict event and surface in the dashboard.
Operations note — kind backfill migration
Deployments that predate the Contact model must run thekind backfill on every tenant DB before shipping this feature (otherwise legacy users become invisible to kind-scoped reads):

