# Custom places

> Add your own places (depots, customer addresses, private POIs) to search, autocomplete, reverse and place lookups of your account, beside the planet index.

Your own places are searched together with the planet index, by your account's keys only. They are stored beside the planet file, never inside it, and no other account can see them.

## Write places

Writes take a **secret key** (`gc_sk_…`). Publishable keys and OAuth tokens get `403 secret_key_required`. Writes count against the key's rate limit, not your geocoding allowance.

```bash
curl -X PUT https://api.geolith.app/v1/places/custom/depot-1 \
  -H "Authorization: Bearer $GEOLITH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Pune depot","names":{"mr":"पुणे डेपो"},"lat":18.5204,"lon":73.8567,"address":{"street":"FC Road","locality":"Pune","country":"IN"},"category":"depot"}'
```

| Method and path                         | Does                                                           |
| --------------------------------------- | -------------------------------------------------------------- |
| `POST /v1/places/custom`                | Write `{ "places": [...] }`, insert or replace by `externalId` |
| `POST /v1/places/custom/bulk`           | Write NDJSON, one place per line                               |
| `PUT /v1/places/custom/{externalId}`    | Write one place                                                |
| `GET /v1/places/custom?after=&limit=`   | List by external id, up to 1,000 a page                        |
| `GET /v1/places/custom/{externalId}`    | Read one                                                       |
| `DELETE /v1/places/custom/{externalId}` | Delete one                                                     |
| `DELETE /v1/places/custom`              | Delete all                                                     |

Writing an `externalId` you deleted earlier brings that place back with the new data.

The OpenAPI 3.1 description is at `https://api.geolith.app/v1/places/openapi.json`. You can also upload an NDJSON file and delete places in the dashboard at [console.geolith.app](https://console.geolith.app/dashboard/places).

## A place

| Field        | Required | Notes                                                                                |
| ------------ | -------- | ------------------------------------------------------------------------------------ |
| `externalId` | yes      | Your id: 1–128 of `A-Z a-z 0-9 . _ ~ -`, unique in your account                      |
| `name`       | yes      | Up to 200 characters                                                                 |
| `names`      | no       | Names by language tag (`hi`, `mr`, `ta`, …); `lang=` picks one                       |
| `lat`, `lon` | yes¹     | WGS 84 degrees; with an outline, the label point (optional, must lie inside)         |
| `geometry`   | no¹      | GeoJSON `Point`, `Polygon` or `MultiPolygon` (RFC 7946, WGS 84)                      |
| `address`    | no       | `housenumber`, `street`, `postalcode`, `locality`, `region`, `country` (ISO alpha-2) |
| `category`   | no       | Returned as `category: [..]`                                                         |
| `expiresAt`  | no       | ISO 8601; after it the place is no longer returned, then deleted                     |

¹ Give `lat` and `lon`, or a `geometry`. `createdAt`, `createdBy`, `updatedAt`, `updatedBy`, `deletedAt` and `deletedBy` are set by the server; a request that sends one gets `400`.

## Outlines: Polygon and MultiPolygon

A place can be an area: a yard, a campus, a delivery zone. Send a GeoJSON `Polygon` (an outer ring, then holes) or `MultiPolygon` (several polygons). Positions are `[longitude, latitude]`.

```bash
curl -X PUT https://api.geolith.app/v1/places/custom/hadapsar-yard \
  -H "Authorization: Bearer $GEOLITH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Hadapsar yard",
    "category": "depot",
    "geometry": {
      "type": "Polygon",
      "coordinates": [
        [[73.920, 18.500], [73.930, 18.500], [73.930, 18.510], [73.920, 18.510], [73.920, 18.500]],
        [[73.924, 18.504], [73.924, 18.506], [73.926, 18.506], [73.926, 18.504], [73.924, 18.504]]
      ]
    }
  }'
```

```bash
curl -X PUT https://api.geolith.app/v1/places/custom/twin-depots \
  -H "Authorization: Bearer $GEOLITH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Twin depots",
    "geometry": {
      "type": "MultiPolygon",
      "coordinates": [
        [[[73.84, 18.52], [73.85, 18.52], [73.85, 18.53], [73.84, 18.53], [73.84, 18.52]]],
        [[[73.88, 18.55], [73.89, 18.55], [73.89, 18.56], [73.88, 18.56], [73.88, 18.55]]]
      ]
    }
  }'
```

Checked on write; a shape that fails gets `400 invalid_place` with the reason:

- Every ring is closed (first and last positions equal), with at least 3 distinct positions and some area.
- Rings do not cross or touch each other or themselves; holes lie inside their outer ring; polygons of a MultiPolygon do not overlap.
- Longitudes in [-180, 180], latitudes in [-90, 90]. A shape across the antimeridian is cut there into a MultiPolygon (RFC 7946 §3.1.9); an edge across more than 180° of longitude is refused.
- At most 10,000 positions per place and 100,000 per request.

Rings are rewound to the right-hand rule (outer rings counter-clockwise, holes clockwise). The place's point is its label point: the centroid of its largest polygon, or a point inside when the centroid falls outside (in a hole or a bend), unless you send `lat`/`lon` inside the shape.

In answers an outline place is returned the way planet places with outlines are:

- `/v1/search`, `/v1/autocomplete`, `/v1/reverse`, `/v1/nearby`: a `Point` at the label point, with the outline's `bbox` (`[west, south, east, north]`; `west > east` across the antimeridian).
- `/v1/place?ids=custom:venue:<externalId>&geometry=true`: the outline as the geometry (`Polygon` for one polygon, else `MultiPolygon`), the label point in `properties.center`, and the `bbox`.
- `/v1/reverse`: a point inside the outline (not in a hole) returns the place whatever the radius; distances are to the label point.

`addendum.custom.geometry_type` says which kind a place is. `GET /v1/places/custom/{externalId}` returns the full `geometry`; the list returns `geometryType`, `bbox` and a small `preview` of each outer ring.

## Limits

| Plan    | Custom places |
| ------- | ------------- |
| Free    | 100           |
| Starter | 1,000         |
| Growth  | 10,000        |
| Scale   | 100,000       |

One request writes at most 500 places in at most 5 MiB (`413 payload_too_large`). A write that would pass your plan's limit is refused whole, with nothing written (`413 custom_places_limit`).

## In answers

- `/v1/search` and `/v1/autocomplete`: a place of yours that holds every word of the query (the last word as a prefix in autocomplete) comes before the planet's places. `boundary.country`, `layers` (custom places are `venue`) and `size` apply.
- `/v1/reverse` and `/v1/nearby`: your places within the radius, merged with the planet's by distance.
- `/v1/place`: ask for `custom:venue:<externalId>` together with planet gids; answers keep the asked order.

Features have `source: "custom"`, `gid: "custom:venue:<externalId>"`, and `addendum.custom` with the names and expiry. A change made with the API shows in answers at once from the same location and everywhere within a minute. Structured search matches the `address` and `locality` words, and the MCP tools and A2A operations return your places the same way as the matching REST route.

## Privacy

Places are visible only to your account's keys. They stop being returned when you delete them, when they expire, and when you close your account. Deleted places are kept, hidden, so we can restore them if you ask; to have them erased for good, write to contact@geolith.app.
