Before you begin, ensure you have:
- A Square Developer account
- A Square application created in the Developer Console
- An access token (personal access token or OAuth token with
REPORTING_READscope) - Basic familiarity with REST APIs and JSON
- A tool for making HTTP requests (cURL, Postman, or your preferred HTTP client)
Did you know?
New to Cube? The Reporting API is built on Cube's semantic layer. We explain all the key concepts (cubes, measures, dimensions) in this guide, but if you want to dive deeper, see the Cube documentation.
Note
- OAuth Support: The Reporting API supports OAuth tokens with the
REPORTING_READpermission. Personal access tokens are also supported for testing and development. - Security Best Practice: Never commit access tokens to source control. Use environment variables or secure secret management systems.
Store your access token as an environment variable:
# macOS/Linux export SQUARE_ACCESS_TOKEN="your_access_token_here" export SQUARE_API_BASE="https://connect.squareup.com/reporting" # Windows (PowerShell) $env:SQUARE_ACCESS_TOKEN="your_access_token_here" $env:SQUARE_API_BASE="https://connect.squareup.com/reporting"
Test your authentication by calling the metadata endpoint:
curl -X GET "${SQUARE_API_BASE}/v1/meta" \ -H "Authorization: Bearer ${SQUARE_ACCESS_TOKEN}" \ -H "Content-Type: application/json"
Expected response: A JSON object containing cubes, measures, dimensions, and segments.
If you receive a 403 Forbidden error, verify:
- Your access token is correct
- You're using a personal access token (not OAuth)
- Your token has not expired
The /v1/meta response shows all available data, including views and cubes. Views are the recommended starting point — they combine data from multiple cubes into a simplified, curated interface.
Let's look at the Sales view:
curl -X GET "${SQUARE_API_BASE}/v1/meta" \ -H "Authorization: Bearer ${SQUARE_ACCESS_TOKEN}" | jq '.cubes[] | select(.name == "Sales") | {name, type, title, measures: [.measures[].name]}'
This command:
- Fetches metadata
- Filters to the
Salesview - Shows the view name and available measures
Sample output:
{ "name": "Sales", "type": "view", "title": "Sales", "measures": [ "Sales.top_line_product_sales", "Sales.net_sales", "Sales.total_collected_amount", "Sales.order_count", "Sales.avg_net_sales", "Sales.discounts_amount", "Sales.itemized_returns", "Sales.refunds_by_amount_amount", "Sales.tips_amount", "Sales.sales_tax_amount" ] }
Note
Views vs Cubes: Views (like Sales) pre-join data from multiple cubes and pre-filter for common use cases. Cubes (like Orders) provide raw, granular access. For most reporting, start with views. See Metadata Discovery for details.
Let's query daily net sales for the last 30 days using the Sales view:
curl -X POST "${SQUARE_API_BASE}/v1/load" \ -H "Authorization: Bearer ${SQUARE_ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "query": { "measures": ["Sales.net_sales"], "timeDimensions": [{ "dimension": "Sales.local_reporting_timestamp", "dateRange": "last 30 days", "granularity": "day" }] } }'
What this query does:
- Measures: Calculates
net_sales(gross sales minus discounts/returns) - Time Dimensions: Groups by day for the last 30 days
- No segment needed: The
Salesview automatically filters to closed (completed) orders
Success response (HTTP 200):
{ "data": [ { "Sales.local_reporting_timestamp.day": "2024-02-01T00:00:00.000", "Sales.net_sales": "1250.50" }, { "Sales.local_reporting_timestamp.day": "2024-02-02T00:00:00.000", "Sales.net_sales": "980.25" } ], "annotation": { "measures": {}, "dimensions": {}, "timeDimensions": {} }, "slowQuery": false }
Note
Note: Access your data via response.data.
Long-running query response (HTTP 200):
{ "error": "Continue wait" }
If you receive "Continue wait", the query is still processing. Retry the same request until you get results. This is expected behavior for complex queries.
Let's break down sales by location name:
curl -X POST "${SQUARE_API_BASE}/v1/load" \ -H "Authorization: Bearer ${SQUARE_ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "query": { "measures": [ "Sales.net_sales", "Sales.tips_amount", "Sales.order_count" ], "dimensions": ["Sales.location_name"], "timeDimensions": [{ "dimension": "Sales.local_reporting_timestamp", "dateRange": "last 30 days", "granularity": "day" }] } }'
Response structure:
{ "data": [ { "Sales.location_name": "Downtown Store", "Sales.local_reporting_timestamp.day": "2024-02-01T00:00:00.000", "Sales.net_sales": "850.50", "Sales.tips_amount": "125.00", "Sales.order_count": "42" }, { "Sales.location_name": "Uptown Store", "Sales.local_reporting_timestamp.day": "2024-02-01T00:00:00.000", "Sales.net_sales": "400.00", "Sales.tips_amount": "50.00", "Sales.order_count": "28" } ], "slowQuery": false }
Notice how the Sales view gives you location_name directly — no need to look up location IDs separately.
For queries that take time to process, implement retry logic:
#!/bin/bash QUERY='{ "measures": ["Sales.net_sales"], "timeDimensions": [{ "dimension": "Sales.local_reporting_timestamp", "dateRange": ["2024-01-01", "2024-12-31"], "granularity": "month" }] }' MAX_RETRIES=10 RETRY_DELAY=2 for i in $(seq 1 $MAX_RETRIES); do echo "Attempt $i of $MAX_RETRIES..." RESPONSE=$(curl -s -X POST "${SQUARE_API_BASE}/v1/load" \ -H "Authorization: Bearer ${SQUARE_ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"query\": $QUERY}") if echo "$RESPONSE" | grep -q "Continue wait"; then echo "Query still processing, waiting ${RETRY_DELAY}s..." sleep $RETRY_DELAY else echo "Results received!" echo "$RESPONSE" | jq '.' exit 0 fi done echo "Query timed out after $MAX_RETRIES attempts" exit 1
import os import time import requests SQUARE_ACCESS_TOKEN = os.environ['SQUARE_ACCESS_TOKEN'] SQUARE_API_BASE = os.environ['SQUARE_API_BASE'] def execute_query_with_retry(query, max_retries=10, retry_delay=2): """Execute a Reporting API query with automatic retry for long-running queries.""" headers = { 'Authorization': f'Bearer {SQUARE_ACCESS_TOKEN}', 'Content-Type': 'application/json' } for attempt in range(1, max_retries + 1): print(f"Attempt {attempt} of {max_retries}...") response = requests.post( f'{SQUARE_API_BASE}/v1/load', headers=headers, json={"query": query} ) if response.status_code != 200: raise Exception(f"API error: {response.status_code} - {response.text}") data = response.json() if 'error' in data and data['error'] == 'Continue wait': print(f"Query still processing, waiting {retry_delay}s...") time.sleep(retry_delay) else: print("Results received!") return data raise TimeoutError(f"Query timed out after {max_retries} attempts") # Example usage query = { "measures": ["Sales.net_sales"], "timeDimensions": [{ "dimension": "Sales.local_reporting_timestamp", "dateRange": ["2024-02-01", "2024-02-29"], "granularity": "day" }] } results = execute_query_with_retry(query) print(results)
const axios = require('axios'); const SQUARE_ACCESS_TOKEN = process.env.SQUARE_ACCESS_TOKEN; const SQUARE_API_BASE = process.env.SQUARE_API_BASE; async function executeQueryWithRetry(query, maxRetries = 10, retryDelay = 2000) { const headers = { 'Authorization': `Bearer ${SQUARE_ACCESS_TOKEN}`, 'Content-Type': 'application/json' }; for (let attempt = 1; attempt <= maxRetries; attempt++) { console.log(`Attempt ${attempt} of ${maxRetries}...`); try { const response = await axios.post( `${SQUARE_API_BASE}/v1/load`, { query }, { headers } ); if (response.data.error === 'Continue wait') { console.log(`Query still processing, waiting ${retryDelay / 1000}s...`); await new Promise(resolve => setTimeout(resolve, retryDelay)); } else { console.log('Results received!'); return response.data; } } catch (error) { throw new Error(`API error: ${error.response?.status} - ${error.message}`); } } throw new Error(`Query timed out after ${maxRetries} attempts`); } // Example usage const query = { measures: ['Sales.net_sales'], timeDimensions: [{ dimension: 'Sales.local_reporting_timestamp', dateRange: ['2024-02-01', '2024-02-29'], granularity: 'day' }] }; executeQueryWithRetry(query) .then(results => console.log(JSON.stringify(results, null, 2))) .catch(error => console.error(error));
Note
There is a delay between when a transaction occurs and when it appears in the Reporting API. Most cubes have approximately a 15-minute delay. Some cubes may offer faster freshness but might not be joinable with other cubes. Check the metadata endpoint (/v1/meta) for per-cube freshness details.
For real-time order data, use the Orders API instead.