Deployed apps resolve a user's city / region / country - from their IP, or from lat/lon coordinates - through a first-party service endpoint. No third-party geocoder, no account, no API key.
Reach for this before pulling in BigDataCloud, Nominatim, ipapi, or any other external geocoder: this service is free, rate-limit-friendly, and keeps user coordinates off third parties.
This is the app service (deployed apps call it over HTTPS). The agent-side
get_locationtool is a separate thing - see location.
Endpoints
POST https://a.gipity.ai/api/<PROJECT_GUID>/services/location/ipPOST https://a.gipity.ai/api/<PROJECT_GUID>/services/location/geocode
Both take an X-App-Token header - the same auth pattern app-llm, app-image, and app-tts use.
/location/ip- body{}(the caller's IP) or{ ip: '8.8.8.8' }(a specific IP)/location/geocode- body{ lat, lon }to reverse-geocode coordinates into a place
Response shape
Both endpoints return the same unified object under data:
{
source: 'ip' | 'geocode',
city: string | null,
region: string | null,
country: string | null,
timezone: string | null,
lat: number | null,
lon: number | null,
}
Fields that can't be resolved are null. Private IPs (10.*, 192.168.*, 172.16-31.*, 127.0.0.1) return an error - there's no useful answer for those.
Whose IP does /services/location/ip see?
Whoever made the HTTP request. This trips people up:
| Who calls it | Body | Resolved IP |
|---|---|---|
| Browser (frontend) | {} |
The user's IP ✓ |
| Function (your backend) | {} |
The sandbox's IP - never what you want ✗ |
| Function (your backend) | { ip: ctx.headers['x-forwarded-for']?.split(',')[0].trim() } |
The user's IP ✓ |
If you just want "what city is the user from?" the simplest answer is: have the browser call it and pass the city into your function as part of the body. One call, no plumbing. See Pattern A.
If you need a server-side lookup (e.g. to log or gate on it), forward the user's IP via the X-Forwarded-For header that the platform puts in ctx.headers. See Pattern B.
Pattern A - Frontend JS (deployed web app)
// 1. Get an app token - absolute URL to the API server, not relative
const tokenRes = await fetch('https://a.gipity.ai/api/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ app: '<PROJECT_GUID>' }),
});
const { data: { token } } = await tokenRes.json();
// 2. Call the location service
const geoRes = await fetch(
`https://a.gipity.ai/api/<PROJECT_GUID>/services/location/ip`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-App-Token': token },
body: JSON.stringify({}), // empty body = caller's IP; or { ip: '8.8.8.8' }
},
);
const { data } = await geoRes.json();
// data = { source, city, region, country, timezone, lat, lon }
Reverse-geocode: POST /services/location/geocode with { lat, lon } - useful after a browser navigator.geolocation fix to turn coords into a city name.
Pattern B - Serverless function (your own backend function)
A function's only injected APIs are { db, fetch, secrets, env, console }. There is NO location object - call the HTTPS endpoint via fetch. Since ctx doesn't include the project GUID, stash it in env vars (gipity env set PROJECT_GUID ...).
export default async function myFn(ctx, { fetch, env }) {
// 1. Get an app token for THIS project
const tokenRes = await fetch('https://a.gipity.ai/api/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ app: env.PROJECT_GUID }),
});
const { data: { token } } = await tokenRes.json();
// 2. Call the location service. To get the USER's city (not the sandbox's),
// forward the original X-Forwarded-For - the platform puts it in ctx.headers.
const userIp = ctx.headers['x-forwarded-for']?.split(',')[0].trim();
const geoRes = await fetch(
`https://a.gipity.ai/api/${env.PROJECT_GUID}/services/location/ip`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-App-Token': token },
body: JSON.stringify({ ip: userIp }),
},
);
const geo = (await geoRes.json()).data;
return { city: geo.city, country: geo.country };
}
Required gipity.yaml permissions (see deploy):
- name: myFn
auth: public
fetch_domains: ['a.gipity.ai'] # REQUIRED - fetch is blocked without it
services: ['location'] # declarative; keep honest
Common mistakes to avoid
- Do not write
services.location.get(...)or{ db, location }-locationis not injected. Usefetch. - Do not use relative URLs from a deployed app (
/api/token,/services/location/ip) - they hit the app CDN host, not the API. Always absolute:https://a.gipity.ai/.... - Do not forget
fetch_domains: ['a.gipity.ai']in function permissions - the sandbox blocks undeclared-domain fetches. ctxdoes not contain the project GUID. Useenv.PROJECT_GUID(set viagipity env set) or a secret.- Calling
/location/ipfrom a function with an empty body resolves the sandbox's IP, not the user's. Forwardx-forwarded-for.
Related skills
- location - the agent-side
get_locationtool and user-scoped CLI/REST routes - app-development - writing functions,
gipity.yaml, andfetchpermissions