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/citystored on a person's contact record. This is declared-only: it comes from what you pass inidentify(), 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:
| Field | Example | Source |
|---|---|---|
country | Turkey | MaxMind (country name from ISO code) |
country_code | TR | MaxMind (ISO 3166-1 alpha-2) |
region | Istanbul | MaxMind |
region_code | 34 | MaxMind |
city | Istanbul | MaxMind |
postal_code | 34000 | MaxMind |
timezone | Europe/Istanbul | MaxMind |
latitude / longitude | 41.0, 28.9 | MaxMind — 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
- Pass
countryandcityin youridentify()traits (from the account or shipping address the customer actually entered). - 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 events | Automatic (IP → MaxMind). Nothing to do. |
| Lat/lon on events | Automatic when analytics consent = granted |
Contact profile country / city | Pass traits.country / traits.city in identify() |
| Location targeting (By Geo segment) | Automatic — reads event country |
| Override a geo field on one event | Pass 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.