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_location tool is a separate thing - see location.

Endpoints

POST https://a.gipity.ai/api/<PROJECT_GUID>/services/location/ip POST 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.

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

Related skills