---
title: "Make your first query"
description: "Get access, create an API key, and run your first query against the catalog API, with cURL, and the response shapes explained."
slug: "/docs/guides/first-query"
last_updated: "2026-09-01"
status: "published"
industry: []
location_types: []
tags: ["get-started", "api", "quickstart"]
is_case_study: false
locked: false
related_metrics: []
related_datasets: []
example_count: 0
example_tools: []
---


# Make your first query

Get access, create an API key, and run your first query against the catalog API, with cURL, and the response shapes explained.

## Metadata

- Tags: get-started, api, quickstart

This guide walks through getting access, creating a key, picking a dataset, and running a first request against the Pine59 Catalog API.

By the end you have a working `curl` command that returns foot-traffic rows from a dataset your account can read.

---

## Before you start

You need:

- A login to the [Pine59 Console](https://console.pine59.com/).
- An API key (created in step 2).
- A terminal with `curl` installed. Any HTTP client works (`httpie`, Postman, or your language of choice), but the snippets in this guide use `curl`.

> **Running requests from the browser.** Once you have a key, every request in this guide, and every other endpoint, can be run from the [API reference](/docs/api). Paste the key into the **Authentication** card at the top and use the "Try it" panel on any operation; the request runs from the browser, no terminal needed.

> **No Console login yet?** Email [support@pine59.com](mailto:support@pine59.com) to have access provisioned. Each login is tied to an account, and the account governs which datasets you can query.

---

## 1. Log in to the Console

Go to [console.pine59.com](https://console.pine59.com/) and sign in with the email address support provisioned.

If the Console shows a "you don't have access to anything here" message after login, the account exists but no datasets are attached yet. Reply to the provisioning email or contact support to have the right subscriptions attached.

---

## 2. Create an API key

Open the API keys page directly: [console.pine59.com/developer/api-keys](https://console.pine59.com/developer/api-keys).

> In the Console, the same page is under **Developer** in the left sidebar, then **API keys**.

Then:

1. Click **Create API key**.
2. Give it a name (e.g. `my-laptop`, `staging-pipeline`) so you know later where it is used.
3. Pick an **Expiry period**: 30, 60, or 90 days, or **No expiry** to keep the key valid until you revoke it. The default is 30 days.
4. Copy the key shown in the dialog and store it somewhere safe. **This is the only time the full key is displayed**. If you lose it, revoke it and create a new one.

API keys look like `uc_…`. They stop working when their expiry date passes (if you set one) or when you revoke them from the same page.

Export the key into your shell so the rest of the guide can reference it:

```bash
export PINE59_API_KEY="uc_paste_your_key_here"
```

---

## 3. Pick a data reference you have access to

Every request targets a single dataset, identified by a **data reference name** like `visitation_usa_places.foot_traffic_month`. Your account governs which ones you can read.

The fastest ways to find one:

- **Browse the catalog**. [console.pine59.com/datasets](https://console.pine59.com/datasets) lists every dataset, with the data reference name shown on each detail page.
- **Browse the docs**. The [Datasets](/docs/datasets) section groups datasets by country and shows the same names.
- **Ask the API**. List everything your key can see:

  ```bash
  curl https://api.pine59.com/v3/datasets \
    -H "Authorization: Bearer $PINE59_API_KEY"
  ```

A rough map from question to dataset:

| You want to…                                    | Reach for                                        |
| ----------------------------------------------- | ------------------------------------------------ |
| Compare stores, brands, or venues               | Foot traffic (place)                             |
| Know who visits                                 | Visitor demographics / visitor profile           |
| See where visitors come from                    | Trade area                                       |
| See what else your visitors visit               | Visitor journey                                  |
| Track a neighborhood, district, or region       | Area foot traffic (visitor / worker / resident)  |

A detail that saves joins: metric datasets embed the location's descriptive fields (in the US places datasets that includes `brands`, `city`, `region`, `us_cbsa`, and `top_category`), so you can filter on them directly in the metric dataset itself.

Pick one and note the name. It goes into the next step.

> The examples below use `visitation_usa_places.foot_traffic_month` (monthly foot traffic at US places). If your account does not have access to it, swap in any data reference from the list above. The request shape is identical.

---

## 4. Make your first request

The simplest first call is `:runQuery` on a dataset. It returns rows from any dataset you have access to. The data reference name goes in the URL, so the body can be almost empty.

```bash
curl -X POST "https://api.pine59.com/v3/datasets/visitation_usa_places.foot_traffic_month:runQuery" \
  -H "Authorization: Bearer $PINE59_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "pageSize": 10
  }'
```

The JSON response looks roughly like this:

```json
{
  "records": [
    {
      "fields": {
        "location_id": "zzz-22c@63p-x7x-mx5",
        "observation_start_date": "2025-04-01",
        "observation_end_date": "2025-04-30",
        "visits_sum": "5940.0000",
        "visits_p50": "200.0000",
        "confidence_score": "0.5050"
      }
    }
  ],
  "fieldDefinitions": [
    { "name": "location_id", "type": "COLUMN_TYPE_STRING" },
    { "name": "visits_sum",  "type": "COLUMN_TYPE_INTEGER" }
  ],
  "totalSize": 5234812,
  "nextPageToken": "…"
}
```

A few things to notice:

- **`fields`** is a flat string→string map. Numeric fields come back as strings. Use `fieldDefinitions` to know what to cast.
- **You got the default field set, not every column.** A request without a `fields[]` array returns a small default set: above, the metric columns only. Descriptive location columns the dataset also carries (`brands`, `city`, `region`, `us_cbsa`, `top_category`) are returned **only when you ask for them by name**:

  ```json
  "fields": [
    { "name": "location_id" },
    { "name": "brands" },
    { "name": "city" },
    { "name": "visits_sum" }
  ]
  ```

  Each entry is an object with a `name`, not a bare string. If a column you expected is missing from the response, this is almost always why. The dataset's Schema tab marks which columns are included by default.
- **`fieldDefinitions`** also tells you which operators are valid on each field (`==`, `<`, `in`, etc.), which is useful when you start filtering.
- **`totalSize`** is the size of the full result set (across all pages), not just this page. Useful for pagination planning.
- **`nextPageToken`** is the cursor for the next page. Pass it back as `pageToken` on the next request to keep paging.

> If you get a `401 Unauthenticated`, the API key was not sent correctly. Make sure you used `Bearer $PINE59_API_KEY` and that the environment variable expanded.
>
> If you get a `403 PermissionDenied` or `NotFound` on the data reference, your account does not have access to that specific dataset. Go back to step 3 and pick one you do have access to.
>
> If a query returns **0 rows**, the most common cause is a filter value that does not exist exactly as written. Values must match the catalog exactly (`ALDI`, not `Aldi`). Re-check with `:searchFieldValues` (step 6) before concluding there is no data.

---

## 5. Filter, aggregate, paginate

Once the bare call works, the same endpoint takes filters, aggregations, and grouping. A more useful request, total visits in January 2025 for a single location:

```bash
curl -X POST "https://api.pine59.com/v3/datasets/visitation_usa_places.foot_traffic_month:runQuery" \
  -H "Authorization: Bearer $PINE59_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": [
      { "name": "visits_sum", "aggregation": "SUM" }
    ],
    "filters": [
      { "fieldName": "location_id", "operator": "==", "value": "PUT_A_REAL_LOCATION_ID_HERE" },
      { "fieldName": "observation_start_date", "operator": ">=", "value": "2025-01-01" },
      { "fieldName": "observation_end_date",   "operator": "<=", "value": "2025-01-31" }
    ],
    "pageSize": 1
  }'
```

The response for an aggregated query suffixes the field name with the aggregation. For an H-E-B in San Antonio (`zzw-222@8sz-ty2-kj9`) it looks like this:

```json
{
  "records": [
    { "fields": { "visits_sum_SUM": "138296.0000" } }
  ],
  "totalSize": 1
}
```

A few shape notes:

- Equality uses `==` (not `=`). Supported operators: `==`, `!=`, `>`, `<`, `>=`, `<=`, `in`, `not in`.
- `in` and `not in` take a `"values"` **array** instead of `"value"`: `{ "fieldName": "brands", "operator": "in", "values": ["Kroger", "ALDI"] }`.
- Aggregations live **inside** the `fields[]` array. You pick a field and attach an `aggregation` (`SUM`, `AVG`, `MIN`, `MAX`, `COUNT`, `COUNT_DISTINCT`). There is no separate top-level `aggregations` key. The result key is `{fieldName}_{AGGREGATION}` in UPPER CASE, e.g. `visits_sum_SUM`, `visits_p50_AVG`.
- To group by a column, add `"groupBy": [{ "fieldName": "location_id" }]`.
- To sort, add `"options": { "orderBy": { "fieldName": "visits_sum", "direction": "DESC" } }`. When ordering by an aggregated field, use the **base** name (`visits_sum`), not the suffixed result key (`visits_sum_SUM`).
- For `OR` logic, set `"filterLogic": "($0 OR $1) AND $2"` referencing filters by 0-based index.

For this request to return anything you need real values to filter on: a `location_id` that exists in the dataset, a date range that is populated, and so on. The next section covers that.

The full filter / aggregation / grouping syntax is documented in the [API reference](/docs/api).

---

## 6. Find values to filter and group by

`:searchFieldValues` returns the distinct values present in a given field. It is the way to find a `location_id`, a date, a region, or any other column value before you build a filter.

List the first ten `location_id`s present in the dataset:

```bash
curl -X POST "https://api.pine59.com/v3/datasets/visitation_usa_places.foot_traffic_month:searchFieldValues" \
  -H "Authorization: Bearer $PINE59_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "field": { "name": "location_id" },
    "pageSize": 10
  }'
```

The response is a flat array of values plus a `fieldDefinition` that tells you the type and which operators that column supports:

```json
{
  "values": [
    "zzw-223@8dj-ds7-cbk",
    "zzy-222@5px-rh9-j5f",
    "226-222@5r6-2b2-qpv"
  ],
  "fieldDefinition": {
    "name": "location_id",
    "type": "COLUMN_TYPE_STRING",
    "filters": { "supportedOperators": ["IN", "=="] }
  },
  "nextPageToken": "…"
}
```

Narrow by a substring with `term`, which works on string fields:

```bash
curl -X POST "https://api.pine59.com/v3/datasets/visitation_usa_places.foot_traffic_month:searchFieldValues" \
  -H "Authorization: Bearer $PINE59_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "field": { "name": "location_id" },
    "term": "zzw",
    "pageSize": 10
  }'
```

Plug any value you get back into the `filters` of a `:runQuery` request (step 5) and you have a working filtered query.

**How `term` matches.** Searching and filtering behave differently, and the difference matters:

| | `term` in `:searchFieldValues` | `value` in a `:runQuery` filter |
| --- | --- | --- |
| Case | Ignored: `heb`, `HEB`, `Heb` behave identically | Exact: `ALDI` matches, `Aldi` does not |
| Position | Matches anywhere in the value | Whole value must match |
| Punctuation | Matched literally | Matched literally |

Three consequences to know before trusting an empty result:

- **Punctuation is part of the value.** `term: "heb"` returns nothing for **H-E-B**, because the stored value contains hyphens. Search `"h-e-b"` instead.
- **Substrings match mid-word.** `term: "aldi"` returns `ALDI`, but also `Grimaldi's` and `Marina Rinaldi`. Read the list before filtering on it.
- **One brand can have several values.** Searching `"Kroger"` returns `Kroger` alongside `Kroger Fuel Center`, `Kroger Pharmacy`, and others. Filtering on `Kroger` alone gets you the supermarkets only, which is usually what you want; decide deliberately. Use `in` with an explicit list when you want several.

> **Aggregations and grouping have the same rule.** Before you `groupBy` a field, run `:searchFieldValues` on it to confirm the values you expect exist. Empty groups are usually a wrong field name, not missing data.

---

## Where to go next

- **[API reference](/docs/api)**: every endpoint, every field, with an interactive "Try it" panel that uses the key you just created.
- **[Authentication](/docs/api#description/authentication)**: keeping the key secret, rotating it, and locking it to specific IP ranges.
- **[Pagination](/docs/api#description/pagination)**: how `pageSize`, `pageToken`, and `totalSize` fit together.
- **[Errors](/docs/api#description/errors)**: the full error-code catalogue and what to retry vs. what to surface.
- **[Datasets](/docs/datasets)**: browse by country to find data references you have access to.
- **[Match your locations](/docs/guides/match-locations)**: bring your own list of stores or sites and query our data using your IDs.
- **[Build with AI](/docs/guides/build-with-ai)**: point Claude, Cursor, or any AI client at these docs and let it explore the catalog for you, without ever putting your key in the chat.

For anything the docs do not cover, email [support@pine59.com](mailto:support@pine59.com).
