Skip to content

User data

Events say what a visitor did. User data says who the visitor is. Two calls carry it:

  • identify stores traits on the visitor’s profile: an email, a name, a plan, a loyalty tier, anything you want your apps to know about the person.
  • alias attaches an identifier you own, such as an email, a phone number or a customer id, to the visitor, so that every browser, device and backend call that presents the same identifier reaches one person.

The difference matters. An email sent as a trait is stored and nothing more, because a form field is not proof that the browser belongs to that person. Only alias links the browser to a person profile. If you remember one thing from this page: send the email as a trait so apps can use it, and alias it so the graph can link on it. The ID graph page explains why, and what a person profile holds. This page covers the calls.

Every call below works from four places. Pick one tab and the whole page follows.

The snippet from your site’s Snippet page binds window.qinto. A call made before the tag has loaded is queued and sent once the connection is open. Every call returns a promise, and two calls can reach the edge in the other order unless you await the first. See Browser.

Call identify whenever you learn something about the visitor: at sign-up, at login, when a profile is edited, when a plan changes. Traits merge, so send only what changed. A trait sent as null clears the stored value.

<script>
qinto.identify({ email: 'ada@example.com', first_name: 'Ada', plan: 'pro' })
</script>

A second argument links an identifier in the same call, so identify(traits, id) is identify(traits) followed by alias(id):

qinto.identify({ email: 'ada@example.com', plan: 'pro' }, 'crm-user-8412')

Eight trait keys are the person’s identity fields. Qinto validates and normalises them, stores them under these names, and apps read them by these names, for example to match a purchase to an ad click. Send them whenever you have them. The more of them a profile holds, the better every destination can match the person.

Key Meaning How Qinto stores it
email Email address Trimmed and lowercased. Must look like name@host.tld, or the trait is dropped.
phone Phone number Trimmed; spaces, (), . and - removed; a leading + kept. Needs at least 7 digits and no other characters, or the trait is dropped.
first_name Given name As sent
last_name Family name As sent
country Country As sent
state State, province or region As sent
city City As sent
postal_code Postal or ZIP code As sent
qinto.identify({ first_name: 'Ada', email: ' Ada@Example.com ', phone: '+1 (415) 555-0142' })
// stored: first_name 'Ada', email 'ada@example.com', phone '+14155550142'
  • Write the keys in snake_case. That is the name Qinto stores and the name a profile read returns. A camelCase key such as firstName is folded to first_name on the way in.
  • A dropped value changes nothing. An invalid email or phone leaves the stored one in place, and the other traits in the same call still apply.
  • Send phone numbers in international format. +14155550142 is unambiguous. A number without a country code is stored as you sent it, and a destination that expects E.164 may not match it.
  • Values are not hashed. Send the plain value over HTTPS. The app that delivers to a destination hashes it where that destination requires a hash.
  • Do not send a standard key for something else. state: 'active' on a subscription overwrites the person’s region. Give the subscription its own key, such as subscription_status.

Any other key is a custom trait. It converges on the person profile like a standard trait, so a plan set on the phone is visible on the laptop. Use custom traits for whatever your apps or your own reads need: a plan, a loyalty tier, a signup date, a segment.

  • Keys are stored in snake_case. loyaltyTier is stored as loyalty_tier, and signupDate as signup_date. Read a trait back by its snake_case name.
  • A string is stored as is. Any other value, such as a number, a boolean or an object, is stored as its JSON text.
  • A key set to null clears the trait.
  • An app that does not know a trait ignores it.

Call alias with an identifier you own at the moment you learn that this browser belongs to that person: after login, after sign-up, after an order is placed with an email. The graph looks the identifier up. If nobody owns it, a person profile is created and this browser’s history, including purchases made before login, moves into it. If a person already owns it, this browser joins that person and receives the traits and consent the other devices contributed.

<script>
qinto.alias('ada@example.com')
</script>

Qinto reads the value you pass and classifies it. You never say which kind it is.

You send Qinto treats it as Normalised to
Ada@Example.com An email ada@example.com
+1 (415) 555-0142 A phone number, because it starts with + and has at least 7 digits +14155550142
crm-user-8412, 4155550142, a UUID A custom id Trimmed, otherwise as sent

A phone number without a leading + is a custom id, not a phone. It still links, but only to calls that send the exact same string, so pick one format and use it everywhere. The same goes for custom ids: 8412 and crm-user-8412 are two different identifiers.

  • An identifier has one owner, forever. The first person profile to claim it keeps it. Repeating alias with the same value is safe and changes nothing.
  • An identifier that belongs to someone else does not move. If this browser already belongs to one person and presents an identifier a different person owns, the identifier is set aside. The browser keeps its person, the owner keeps the identifier, and two person profiles are never merged. The clash is not lost: getPerson(identifier) on the server returns the owner, and lists the person that also presented it under related. Related persons are unverified, so treat them as a hint for your own review, not as the same person.
  • Shared devices are not special. If two people log in from one browser, the second person’s identifier joins the first person unless it already has an owner, whether it is an email, a phone number or an account id. Nothing you pass to alias isolates two accounts on one browser, and a wrong link is permanent. Call alias only after a successful login, never from a form field before it is verified.
  • Link before you track from the server. A server track addressed by an identifier the site has never seen fails with 404. identify or alias the identifier first, in the same request if you need to.

Sign-up or login in the browser. Store what you know and link the account in one call. The account id is the best identifier, because it never changes; the email is the best trait, because destinations match on it.

qinto.identify({ email: user.email, first_name: user.firstName, last_name: user.lastName }, user.id)

If you want backend calls to be able to name the person by email as well, link both. Await the identify first: if the alias lands first and the email is new, it creates a person of its own, and the account id then arrives as a conflict with the person your backend already created.

await qinto.identify({ email: user.email }, user.id)
await qinto.alias(user.email)

An order webhook on your server. The webhook carries the customer’s email. This runs in your backend with @blotout/qinto-sdk-server, so the first argument names the person. Identify first so the person exists, then track.

import { createClient } from '@blotout/qinto-sdk-server'
const qinto = createClient({ destination: 'https://q.example.com', token: process.env.QINTO_EDGE_TOKEN })
await qinto.identify(order.email, { email: order.email, first_name: order.firstName, country: order.country })
await qinto.track(order.email, 'Purchase', { orderId: order.id, currency: order.currency, value: order.total })

If the browser aliased the same email at checkout, this lands on the person the browser created, and the orderId deduplicates the purchase against the one the page sent.

A CRM sync. Address each record by the id your CRM uses, and send the traits that changed. Records that have never been seen get a person profile. Records already linked from a browser are updated in place, and the new traits reach every linked device.

await qinto.identify(`crm-user-${record.id}`, { plan: record.plan, city: record.city })
  • In the browser, await qinto.getVisitor() returns { anonId, globalId, … }. globalId is null until the visitor is linked to a person, and set afterwards. It asks the edge each time, so it reflects an alias that just completed.
  • From the server, getPerson(identifier) returns the person an identifier belongs to, the visitor ids linked to it, and under related any other person that presented the same identifier and was set aside. An empty related means the identifier has never clashed. It needs the Read visitor ids permission.
  • In the platform, User data looks up any visitor or person by an identifier and shows the traits, identifiers and purchases the graph holds.
  • edgetag('data', { … }) is qinto.identify({ … }). Same shape, same merge behaviour.
  • edgetag('user', key, value) is qinto.identify({ [key]: value }). There is no one-key call.
  • zip is postal_code. The other standard keys keep their names, written in snake_case.
  • gender and dateOfBirth are custom traits. They are stored as gender and date_of_birth and converge like any other trait.
  • A trait does not link devices. Where your EdgeTag code sent the email to connect a visitor to a person, add qinto.alias(email) next to the identify.