# Place lookup (beta)

> Look up one TikTok place and get the posts tagged there, read fresh from TikTok plus the ones Virlo already has, with TikTok's own tag count and what changed since your last lookup.

Source: https://dev.virlo.ai/docs/satellite/places
Markdown: https://dev.virlo.ai/docs/satellite/places.md
Section: Deep-dive lookups

## About the Virlo API (applies to every page)

- Base URL: `https://api.virlo.ai/v1`. Every request needs the header `Authorization: Bearer YOUR_API_KEY` (keys start with `virlo_tkn_`).
- Responses are JSON inside a `data` field, except the `/v1/webhooks` endpoints, which return the object or array directly. Field names are snake_case.
- Prices are in US dollars from a prepaid balance. 1 credit = $0.01. The `X-Cost` response header on each successful response is the exact charge. Errors are free.
- Slow jobs return an ID. Check its status every 15 seconds (or whatever `retry_after_seconds` says) until `finalized` is `true`.
- All docs pages: https://dev.virlo.ai/llms.txt. Every page in one file: https://dev.virlo.ai/llms-full.txt. MCP server for AI assistants: https://dev.virlo.ai/api/mcp/mcp.

---

> **Note:** Beta: TikTok places only. Instagram places are not available yet. A place is the location tag a creator chose on a post, usually where the video was filmed. The numbers are the posts creators tagged at the place, TikTok's own tag count, and how that count changed between lookups. These endpoints describe what's posted at a place, never where one person has been.

See what's being posted at one TikTok place right now, and how its tag count has changed.

**At a glance**

- **What it does:** Reads the posts tagged at one venue or area fresh from TikTok, adds the ones Virlo already has, and sums up their views, creators, hashtags and dates.
- **You send:** The place: a Virlo place id, TikTok's own place id, or its name.
- **You get back:** A `job_id` right away. Check it every 5 to 10 seconds until it says `completed`.
- **Cost:** $0.50 per lookup started. A name that matches several places returns them free, and so does the same place again within 6 hours when you ask for as many posts or fewer.
- **How long:** Usually under a minute. A lookup of the Melbourne Cricket Ground took 8 seconds.

To find a place, or to read only the videos Virlo already has tagged there ($0.05), use [Places](https://dev.virlo.ai/docs/places).

---

## What it costs

You pay when a lookup starts. 1 credit is $0.01.

| What happens | Price |
| - | - |
| A lookup starts | $0.50 (50 credits), for any `max_videos` up to 200 |
| A `query` matches more than one place and you get the list | Free |
| The same place again within 6 hours, asking for as many posts or fewer | Free (`cached: true`, the earlier run) |
| Any `400` or `404` | Free |
| A lookup that fails | Refunded in full, shown as `credits_refunded` |

The exact charge is in the `X-Cost` (dollars) and `X-Credits-Used` (credits) response headers.

---

## Start a place lookup

**Endpoint:** `POST https://api.virlo.ai/v1/satellite/place`

Name the place in exactly one way. Sending none, or more than one, returns `400`.

Cost per request:
$0.50 per lookup started. A list of matching places is free, and so is a repeat within 6 hours that asks for as many posts or fewer.

### Body parameters

- `place_id` (string, optional): A Virlo place id, from [`GET /v1/places`](https://dev.virlo.ai/docs/places#search-places) or a video's `location.id`.
- `poi_id` (string, optional): TikTok's own numeric place id.
- `query` (string, optional): A place name, or the place's TikTok link (`https://www.tiktok.com/place/...`), which names the place exactly. 2 to 500 characters. A name is matched against the places Virlo has stored. The lookup starts when the name picks out one stored TikTok place: one place named exactly that, and no other carrying the name. Any other match (several places with the name, or places that carry it inside a longer name) answers `200` with `"status": "candidates"` (up to 20, plus `total`) and cost nothing: send the one you want as `place_id`. Each candidate has `id`, `poi_id`, `name`, `kind`, `locality`, `city`, `country`, `video_count` (Virlo videos tagged there) and `tiktok_video_count`. `source` says where the candidates came from: `virlo` for stored places, `tiktok_search` when TikTok's own place search answered because no stored place has the name. A `tiktok_search` candidate Virlo has not stored has `id: null`: send its `poi_id`.
- `max_videos` (integer, optional): Most posts to return, newest first, from 1 to 200. Default `100`. The price is the same.

Trend analysis isn't available for places yet.

**cURL by name request:**

```bash
curl -X POST https://api.virlo.ai/v1/satellite/place \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "Melbourne Cricket Ground", "max_videos": 50 }'
```

**cURL by place_id request:**

```bash
curl -X POST https://api.virlo.ai/v1/satellite/place \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "place_id": "b7fa189b-df27-44c5-9f83-e483927c8e18", "max_videos": 50 }'
```

**JavaScript request:**

```js
const response = await fetch('https://api.virlo.ai/v1/satellite/place', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ query: 'Melbourne Cricket Ground', max_videos: 50 }),
})
const body = await response.json()

// Several matches: pick one from body.data.candidates and send its id as place_id
const jobId = body.data.job_id // use this to check the status
```

**Python request:**

```python
import requests

response = requests.post(
    'https://api.virlo.ai/v1/satellite/place',
    headers={'Authorization': 'Bearer YOUR_API_KEY'},
    json={'query': 'Melbourne Cricket Ground', 'max_videos': 50},
)
data = response.json()['data']

# Several matches: pick one from data['candidates'] and send its id as place_id
job_id = data.get('job_id')  # use this to check the status
```

**Response 202 Started:**

```json
{
  "data": {
    "job_id": "c8814bb7-452d-4bab-9ca2-f57eaf81a051",
    "status": "processing"
  }
}
```

**Response 202 Cached:**

```json
{
  "data": {
    "job_id": "c8814bb7-452d-4bab-9ca2-f57eaf81a051",
    "status": "completed",
    "cached": true
  }
}
```

**Response 200 Candidates (shortened):**

```json
{
  "data": {
    "status": "candidates",
    "source": "virlo",
    "message": "The name does not pick out one TikTok place. Choose from the candidates and send its id as place_id. Nothing was charged.",
    "total": 8,
    "candidates": [
      {
        "id": "04c42bb9-3a55-4a4a-a46e-264dc1ed3611",
        "poi_id": null,
        "name": "Chadstone Shopping Centre",
        "kind": "venue",
        "locality": "Malvern East",
        "city": "Stonnington",
        "country": "AU",
        "video_count": 27,
        "tiktok_video_count": null
      },
      {
        "id": "93e59051-6de4-4e0d-8f77-343ca4320fa0",
        "poi_id": null,
        "name": "Chadstone",
        "kind": "area",
        "locality": null,
        "city": "Monash",
        "country": "AU",
        "video_count": 2,
        "tiktok_video_count": null
      },
      {
        "id": "fb40a9af-5650-4415-a686-fd0d1c03ae4c",
        "name": "Myer Chadstone",
        "kind": "venue",
        "locality": "Chadstone",
        "city": "Stonnington",
        "country": "AU",
        "video_count": 2
      }
    ]
  }
}
```

**Response 400:**

```json
{
  "message": "Send exactly one of place_id, poi_id or query",
  "error": "Bad Request",
  "statusCode": 400,
  "code": "validation_error"
}
```

**Response 402:**

```json
{
  "statusCode": 402,
  "message": "Insufficient balance. $0.50 required, $0.30 remaining. Add funds at https://dev.virlo.ai/dashboard/billing",
  "error": "Payment Required",
  "required_credits": 50,
  "remaining_credits": 30,
  "code": "insufficient_credits"
}
```

**Response 404:**

```json
{
  "message": "Place 00000000-0000-4000-8000-000000000000 not found",
  "error": "Not Found",
  "statusCode": 404,
  "code": "not_found"
}
```

---

## Check lookup status

**Endpoint:** `GET https://api.virlo.ai/v1/satellite/place/status/:job_id`

Free. Check every 5 to 10 seconds until `finalized` is `true` (`status` is `completed` or `failed`). This check stops working after 24 hours; the [saved run](https://dev.virlo.ai/docs/satellite/places#durable-runs) doesn't. The `job_id` is also the `run_id`.

**cURL request:**

```bash
curl https://api.virlo.ai/v1/satellite/place/status/c8814bb7-452d-4bab-9ca2-f57eaf81a051 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**JavaScript request:**

```js
const jobId = 'c8814bb7-452d-4bab-9ca2-f57eaf81a051'

const response = await fetch(
  `https://api.virlo.ai/v1/satellite/place/status/${jobId}`,
  { headers: { Authorization: 'Bearer YOUR_API_KEY' } },
)
const body = await response.json()
const { status, finalized, result } = body.data
```

**Python request:**

```python
import requests

job_id = 'c8814bb7-452d-4bab-9ca2-f57eaf81a051'

response = requests.get(
    f'https://api.virlo.ai/v1/satellite/place/status/{job_id}',
    headers={'Authorization': 'Bearer YOUR_API_KEY'},
)
data = response.json()['data']
print(data['status'], data.get('finalized'))
```

**Response Processing:**

```json
{
  "data": {
    "status": "processing",
    "finalized": false
  }
}
```

**Response Completed (shortened):**

```json
{
  "data": {
    "status": "completed",
    "finalized": true,
    "result": {
      "run_id": "c8814bb7-452d-4bab-9ca2-f57eaf81a051",
      "request": {
        "poi_id": "22535865216870910",
        "platform": "tiktok",
        "max_videos": 50,
        "trend_analysis": false
      },
      "place": {
        "id": "b7fa189b-df27-44c5-9f83-e483927c8e18",
        "poi_id": "22535865216870910",
        "platform": "tiktok",
        "name": "Melbourne Cricket Ground",
        "kind": "venue",
        "address": "Brunton Ave, Richmond VIC 3002, Australia",
        "city": "Melbourne",
        "country_code": "AU",
        "lat": -37.819919,
        "lng": 144.98335,
        "video_count": 102349,
        "video_count_source": "page",
        "url": "https://www.tiktok.com/place/Melbourne-Cricket-Ground-22535865216870910"
      },
      "change": {
        "previous_run_id": null,
        "previous_run_at": null,
        "days_since_previous": null,
        "video_count": 102349,
        "previous_video_count": null,
        "video_count_change": null,
        "new_since_previous": null,
        "new_since_previous_ids": []
      },
      "data_captured_at": "2026-11-10T22:08:58.557Z",
      "credits_charged": 50,
      "stats": {
        "videos_analyzed": 50,
        "views": {
          "total": 5127043,
          "avg": 102541,
          "median": 23535,
          "max": 1159625,
          "min": 1272
        },
        "engagement": {
          "total_likes": 252845,
          "total_comments": 2786,
          "total_shares": 14954,
          "total_collects": 14900,
          "avg_likes": 5057,
          "avg_comments": 56,
          "avg_shares": 299,
          "avg_collects": 298,
          "engagement_rate": 0.0557
        }
      },
      "sample_quality": {
        "truncated_by_cap": true,
        "pages_fetched": 4,
        "note": "healthy_corpus"
      },
      "sources": {
        "gateway": 7,
        "stored": 36,
        "both": 7
      },
      "walk": {
        "stop": "no_new",
        "pages_read": 4,
        "posts_seen": 27,
        "nearby_dropped": 0
      },
      "videos": [
        {
          "id": "7690504419732901126",
          "url": "https://www.tiktok.com/@mymcg/video/7690504419732901126",
          "description": "Runnin’ it back ⏪ The biggest day on the footy calendar in 90 seconds #afl #mcg #melbourne ",
          "publish_date": "2026-09-28T08:45:00.000Z",
          "duration_seconds": 83,
          "views": 306150,
          "likes": 16590,
          "comments": 59,
          "shares": 415,
          "collects": 672,
          "hashtags": [
            "afl",
            "mcg",
            "melbourne"
          ],
          "is_duet": false,
          "is_stitch": false,
          "region": "AU",
          "author": {
            "unique_id": "mymcg",
            "nickname": "Melbourne Cricket Ground",
            "is_verified": true,
            "follower_count": 90497
          },
          "source": "stored"
        }
      ]
    }
  }
}
```

**Response Failed:**

```json
{
  "data": {
    "status": "failed",
    "finalized": true,
    "error": "A short message explaining what went wrong",
    "credits_refunded": 50
  }
}
```

**Response 404:**

```json
{
  "message": "Place lookup job not found or expired",
  "error": "Not Found",
  "statusCode": 404,
  "code": "not_found"
}
```

---

## What you get back

- `place` (object, optional): `id` (Virlo's, `null` if Virlo hasn't seen the place), `poi_id`, `name`, `kind` (`venue` or `area`), `address`, `city`, `country_code`, `lat`, `lng`, `url`, and `video_count`: TikTok's own count of posts tagged there, read on this lookup. `video_count_source` is `page` (the place's page) or `search` (TikTok's place search, when the page had no count). `null` when TikTok gives none.
- `change` (object, optional): What's different from your team's previous lookup of this place. [Details below](https://dev.virlo.ai/docs/satellite/places#change-block).
- `videos` (array, optional): The posts tagged at the place, newest first, in the same shape as a [hashtag lookup](https://dev.virlo.ai/docs/satellite/hashtags). Each also has `source`: `gateway` (read fresh on this lookup), `stored` (Virlo already had it), or `both`, and `counts_fresh`: `true` when the post's views and engagement were read from TikTok on this lookup, `false` when they are from the day Virlo last saw the post.
- `stats` (object, optional): Views, engagement, top creators, related hashtags and top sounds over the returned posts. Same fields as [hashtag lookup stats](https://dev.virlo.ai/docs/satellite/hashtags#stats-block). `velocity` (`videos_per_week`, the two 4-week averages and `is_accelerating`) counts the posts read from TikTok on this lookup: a ranked sample of the place, so read the numbers as posts per week in that sample. It also has `comparable`: `true` when those posts reach back eight weeks and `max_videos` cut no older posts off. Read `velocity.is_accelerating` only when `comparable` is `true`; it is `false` otherwise. Lookups saved before Nov 12 2026 have averages over every returned post. How a place is moving is in [`change`](https://dev.virlo.ai/docs/satellite/places#change-block).
- `sources` (object, optional): How many returned posts came each way: `gateway`, `stored`, `both`. `stored_refreshed` is how many of the stored posts had their views and engagement read from TikTok again on this lookup.
- `walk` (object, optional): How reading TikTok's list went: `stop` (`end`, `no_new`, `max_pages`, `failed`, or `not_found`), `pages_read`, `posts_seen`, and `nearby_dropped` (posts on a venue's list tagged at a nearby place, left out; an area keeps them).
- `sample_quality` (object, optional): `truncated_by_cap`, `pages_fetched`, and `note`, as on other lookups.

**A lookup is a wide sample, not every post ever tagged there.** `place.video_count` can be far larger than `videos`: the Melbourne Cricket Ground counts 102,349 tagged posts and one lookup returned 50. TikTok ranks a place's posts rather than listing them by date, so the newest post returned can be weeks old on a big venue.

---

## What changed since last time

Because the list isn't newest first, new activity shows up in two numbers, measured against your team's previous completed lookup of the same place in the last 120 days:

- `video_count_change` (integer | null, optional): TikTok's tag count now minus the count on the previous lookup. `null` without both counts.
- `new_since_previous` (integer | null, optional): Posts this lookup saw that the previous one didn't. It can include older posts TikTok hadn't shown before, so check their `publish_date` before calling them new. `new_since_previous_ids` lists up to 50, matching `videos[].id` when they were kept.
- `previous_run_id, previous_run_at, days_since_previous` (string, string, number, optional): Which lookup this is compared with, and how long ago.
- `video_count, previous_video_count` (integer | null, optional): The two counts behind the change.

On a first lookup every field except `video_count` is `null`: there's nothing to compare yet, which isn't the same as no change. A repeat within 6 hours returns the earlier run, so look again after that.

---

## Re-read past runs

Every paid lookup is saved as its own run. Reopening it is free, with the same tools as other lookups:

- [`GET /v1/satellite/runs/:run_id`](https://dev.virlo.ai/docs/satellite#durable-runs): the full saved result, under `result`.
- `GET /v1/satellite/runs?type=place_lookup`: your past place lookups.
- [`GET /v1/satellite/runs/:run_id/posts`](https://dev.virlo.ai/docs/satellite#run-posts): the posts, a page at a time.
- [`GET /v1/satellite/runs/:run_id/compare`](https://dev.virlo.ai/docs/satellite#compare-runs): this run against an earlier one, including TikTok's tag count. Free.
- [`POST /v1/satellite/runs/:run_id/refresh`](https://dev.virlo.ai/docs/satellite#refresh-run): run the same lookup again for $0.50.

---

## Errors

Rejected requests cost nothing. A started lookup is billed, and refunded in full if it ends `failed`.

- `400 Bad Request`: Not exactly one of `place_id`, `poi_id` and `query`, a bad value, an unknown field, or an Instagram place. Code `validation_error`.
- `401 Unauthorized`: Key missing (`missing_api_key`), or wrong or revoked (`invalid_api_key`). See [Authentication](https://dev.virlo.ai/docs/authentication).
- `402 Payment Required`: Balance under $0.50 when a lookup would start. `required_credits` is in credits (50 = $0.50).
- `404 Not Found`: No such place, no TikTok place matches the name, or a status check for a job that is unknown or over 24 hours old. [Open the saved run](https://dev.virlo.ai/docs/satellite/places#durable-runs) instead.
- `429 Too Many Requests`: Over [the limit](https://dev.virlo.ai/docs/rate-limits#default-limits). Code `rate_limit_exceeded`.
- `502 Bad Gateway`: Virlo couldn't start the lookup. Code `upstream_error`. Retry after a short wait.

A lookup that starts and then fails ends with `"status": "failed"` and `error` on the status check, and the $0.50 comes back as `credits_refunded`. For example, TikTok doesn't know the place, or its page can't be read right now.

---

More in Deep-dive lookups:

- [Creator lookup](https://dev.virlo.ai/docs/satellite.md)
- [Creator lookup add-ons](https://dev.virlo.ai/docs/satellite/creators.md)
- [Video outlier check](https://dev.virlo.ai/docs/satellite/video-outlier.md)
- [Sound lookup](https://dev.virlo.ai/docs/satellite/sounds.md)
- [Hashtag lookup](https://dev.virlo.ai/docs/satellite/hashtags.md)
