# App API - Location Service

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](location.md).

## 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.

- `/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)

```js
// 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 ...`).

```js
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](deploy.md)):

```yaml
- 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 }` - `location` is not injected. Use `fetch`.
- **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.
- **`ctx` does not contain the project GUID.** Use `env.PROJECT_GUID` (set via `gipity env set`) or a secret.
- **Calling `/location/ip` from a function with an empty body** resolves the sandbox's IP, not the user's. Forward `x-forwarded-for`.

## Related skills

- [location](location.md) - the agent-side `get_location` tool and user-scoped CLI/REST routes
- [app-development](app-development.md) - writing functions, `gipity.yaml`, and `fetch` permissions

