Skip to main content

Coming from Segment or PostHog

Call our Contact upsert where you called identify, call our Event route where you called track, and drop everything that existed to tie anonymous visitors to known users. Throughline has no anonymous Events, so that machinery has nothing to do.

Your call todayAPI route@throughlinehq/serverNote
analytics.identify(userId, traits)PUT /v1/contacts/{externalId}contacts.upsert({ externalId: userId, email, … })email is required. A Contact has fixed fields: email, name, phone, timeZone. Other traits are not stored.
posthog.identify(distinctId, properties)PUT /v1/contacts/{externalId}contacts.upsert({ externalId: distinctId, email, … })As above.
analytics.track(userId, event, properties)POST /v1/events/by-external-idevents.track({ eventName, contactExternalId, properties })The Contact must exist first.
posthog.capture(distinctId, event, properties)POST /v1/events/by-external-idevents.track({ eventName, contactExternalId, properties })The Contact must exist first.
analytics.group(userId, groupId, traits)POST /v1/events/by-organizationevents.track({ …, organizationExternalId: groupId })Name the organization on the Event. It has to exist already; see Organizations.
analytics.page(), analytics.screen()POST /v1/events/by-external-idevents.track({ eventName: 'page_viewed', … })A page view is an ordinary Event. From a browser, use @throughlinehq/browser.
analytics.track(…) for an anonymous visitornonenoneEvery Event belongs to a known Contact or organization.
analytics.alias(…)nonenoneThere is no anonymous identity to merge into a known one.
analytics.flush()nonenot neededNothing is buffered. Every call is one awaited request.
a test-mode write keynonenoneUse a separate staging Tenant.

Why there is no identify​

In Segment and PostHog, identify does two jobs: it stores traits, and it ties an anonymous visitor's earlier Events to a user. We do only the first, with an upsert. Throughline exists to send email and SMS to people, and it can only reach someone it knows the address of, so an Event about an unknown visitor has nobody to act on. Rather than hold Events for people who may never become Contacts, we accept Events only about Contacts that exist.

In practice: upsert the Contact when the person signs up, then record Events. An Event about someone you have not upserted yet is refused with 422 unresolved_identity, not silently dropped.

Organizations​

Where Segment has group, we name the organization on the Event. Use POST /v1/events/by-organization for an Event about the organization itself, with an optional contactId for the person who acted. To attribute a Contact's Event to their organization, add organizationExternalId to it on POST /v1/events/by-external-id.

Unlike Segment's group, naming an organization does not create it. Organizations are created in the app, under Organizations → New organization, with the external ID your own system uses for them. An Event that names only an organization that does not exist is refused with 422 unresolved_identity.