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
curlinstalled. Any HTTP client works (httpie, Postman, or your language of choice), but the snippets in this guide usecurl.
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:
- Click Create API key.
- Give it a name (e.g.
my-laptop,staging-pipeline) so you know later where it is used. - 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.
- 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 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.
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:
-
fieldsis a flat string→string map. Numeric fields come back as strings. UsefieldDefinitionsto 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. -
fieldDefinitionsalso tells you which operators are valid on each field (==,<,in, etc.), which is useful when you start filtering. -
totalSizeis the size of the full result set (across all pages), not just this page. Useful for pagination planning. -
nextPageTokenis the cursor for the next page. Pass it back aspageTokenon the next request to keep paging.
If you get a
401 Unauthenticated, the API key was not sent correctly. Make sure you usedBearer $PINE59_API_KEYand that the environment variable expanded.If you get a
403 PermissionDeniedorNotFoundon 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, notAldi). 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. inandnot intake 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 anaggregation(SUM,AVG,MIN,MAX,COUNT,COUNT_DISTINCT). There is no separate top-levelaggregationskey. 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
ORlogic, 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 :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"returnsALDI, but alsoGrimaldi'sandMarina Rinaldi. Read the list before filtering on it. - One brand can have several values. Searching
"Kroger"returnsKrogeralongsideKroger Fuel Center,Kroger Pharmacy, and others. Filtering onKrogeralone gets you the supermarkets only, which is usually what you want; decide deliberately. Useinwith an explicit list when you want several.
Aggregations and grouping have the same rule. Before you
groupBya field, run:searchFieldValueson 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, andtotalSizefit 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.