Geo & location

Geo & location

Tocadule attaches location to your data in two very different places, resolved two very different ways. This page explains both so you know exactly what you get for free and what you have to send us.

  • Event location — country, region, city, postal code, timezone (and, with consent, lat/lon) on every event. Resolved automatically from the visitor's IP. You do nothing.
  • Contact profile location — the country / city stored on a person's contact record. This is declared-only: it comes from what you pass in identify(), and is never guessed from an IP.

Short version: events are located by IP (automatic). Contacts are located only by what you tell us (traits.country / traits.city in identify). Location segments read the event country, not the contact record.

How event location is resolved

When an event arrives, we look up the real client IP against an offline MaxMind GeoLite2 City database. MaxMind is the single source of truth for event location:

FieldExampleSource
countryTurkeyMaxMind (country name from ISO code)
country_codeTRMaxMind (ISO 3166-1 alpha-2)
regionIstanbulMaxMind
region_code34MaxMind
cityIstanbulMaxMind
postal_code34000MaxMind
timezoneEurope/IstanbulMaxMind
latitude / longitude41.0, 28.9MaxMind — only with analytics consent

These land in the event's properties, so properties.country, properties.city, etc. are populated on the event row automatically.

CloudFront is a fallback and provides ASN. On first-party (custom domain) installs CloudFront can supply a country + ASN header; MaxMind still wins for the location fields because it resolves city/region consistently across every distribution. The ASN is used only for bot detection — it never becomes contact or profile location.

If you pass a location field yourself on an event (e.g. country in your track() props), your value wins — we only fill fields you didn't provide.

Precision and consent

Country, region, city, postal, and timezone are always resolved when geo is enabled. Latitude/longitude are gated on the analytics consent lane — we only attach them when consent_state is granted, because coordinate-level data is more sensitive. Geo lookup is fail-safe: any error (bad IP, DB miss) just leaves the field unset and never blocks the event.

Contact profile location is declared-only

A contact's stored country and city come only from the traits you pass to identify():

tocadule.identify('shopper@example.com', {
  first_name: 'Ada',
  last_name: 'Lovelace',
  phone: '+90...',
  country: 'Turkey',   // ← sets contact.country
  city: 'Istanbul'     // ← sets contact.city
});

We do not infer profile location from the request IP, and we never will.

⚠️

An IP tells you where a request came from — a proxy, a VPN, a phone on roaming, a corporate egress, a prefetch bot — not where a person lives. Inferring a contact's home from their first-touch IP and freezing it would permanently mislabel people (a shopper proxied through Frankfurt stamped "Germany" forever). So profile location stays declared-only.

Here is the exact identify script from the product (copy it verbatim):

<script>
  // Call this ONCE, at sign-up / login / newsletter opt-in — it's the richer
  // one-time profile call (sets subscription intent + full name/location).
  // After this you do NOT need to call identify again: just pass email on your
  // events (optional) and we keep the contact linked. Everything except email is
  // optional — country + city unlock location segments.
  if (window.tocadule) {
    tocadule.identify('{email}', { first_name: '{firstName}', last_name: '{lastName}', phone: '{phone}', country: '{country}', city: '{city}' });
  }
</script>

On upsert, country / city are only overwritten by a non-empty declared value — a later identify without those traits never wipes what you already stored.

What you must do to get profile location

  1. Pass country and city in your identify() traits (from the account or shipping address the customer actually entered).
  2. That's it. Nothing else populates contact.country / contact.city.

If you never pass them, the contact simply has no profile location — but the events that contact generated still carry full IP-derived geo for analytics and segmentation.

Location segments are event-based

The By Geo segment (under the Audience lens) filters visitors by the country on their events — it reads properties.country, not the contact record. The available countries and their visitor counts are computed straight from event data:

SELECT properties->>'country' AS country,
       COUNT(DISTINCT COALESCE(anonymous_id, contact_id::text)) AS visitors
FROM events
WHERE workspace_id = $ws
  AND properties->>'country' IS NOT NULL
  AND properties->>'country' <> ''
GROUP BY properties->>'country'

Because event country is IP-resolved automatically, By Geo works for every visitor — anonymous or identified, whether or not you ever called identify(). You don't need to send anything to make geo targeting work; it's on by default.

Analytics dashboards (e.g. Top Countries) read the same event-level properties.country / properties.city. Contact-record country is used for contact-field filters, not for the By Geo audience template.

cart_state never carries location or PII

The passive cart reader and tocadule.cart() carry only cart contents plus the anonymous_id. They are PII-free by design and resolve to a contact through the identity map — never put email, name, or location on the cart. The cart's location, if you need it, comes from the visitor's events.

Timezone

MaxMind supplies an IANA timezone (e.g. Europe/Istanbul) on the event when resolvable. It's an event property like the rest — useful for send-time and reporting — and, like all geo fields, is IP-derived, not declared.

Quick reference

You want…How to get it
Country/city on eventsAutomatic (IP → MaxMind). Nothing to do.
Lat/lon on eventsAutomatic when analytics consent = granted
Contact profile country / cityPass traits.country / traits.city in identify()
Location targeting (By Geo segment)Automatic — reads event country
Override a geo field on one eventPass it yourself in the event props

Related

  • Consent — the analytics lane (consent_state) gates lat/lon; the ad lane (ad_consent) gates forwarding. Email/SMS opt-in is a separate preference, not a consent lane.
  • Identify & the identity contract — how identify() and event-level email link a visitor to a contact.