Back
GuidesMake your first query

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.

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

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:

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 lists every dataset, with the data reference name shown on each detail page.

  • Browse the docs. The Datasets section groups datasets by country and shows the same names.

  • Ask the API. List everything your key can see:

    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 venuesFoot traffic (place)
Know who visitsVisitor demographics / visitor profile
See where visitors come fromTrade area
See what else your visitors visitVisitor journey
Track a neighborhood, district, or regionArea 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.

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:

{
  "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:

    "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:

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:

{
  "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.


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_ids present in the dataset:

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:

{
  "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:

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 :searchFieldValuesvalue in a :runQuery filter
CaseIgnored: heb, HEB, Heb behave identicallyExact: ALDI matches, Aldi does not
PositionMatches anywhere in the valueWhole value must match
PunctuationMatched literallyMatched 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: every endpoint, every field, with an interactive "Try it" panel that uses the key you just created.
  • Authentication: keeping the key secret, rotating it, and locking it to specific IP ranges.
  • Pagination: how pageSize, pageToken, and totalSize fit together.
  • Errors: the full error-code catalogue and what to retry vs. what to surface.
  • Datasets: browse by country to find data references you have access to.
  • Match your locations: bring your own list of stores or sites and query our data using your IDs.
  • 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.

Did you find what you were looking for?