# Run a SQL query

Execute ad-hoc SQL against your Stripe data.

You can execute SQL against your Stripe data by creating a `QueryRun`. Use the same SQL syntax as the [Sigma query editor](https://docs.stripe.com/data/write-queries.md) to automate extracts, run queries on your own schedule, and load results into your systems.

A `QueryRun` is an asynchronous job. You send SQL, Stripe starts executing it, and you [retrieve the QueryRun](https://docs.stripe.com/data/api/query-runs.md#retrieve-query-run) later to get a CSV or inline rows. Learn more about [runs](https://docs.stripe.com/data/api.md#concepts).

> #### Sigma subscription required
> 
> Creating a query run requires an active [Sigma](https://stripe.com/sigma) subscription.

## Required permissions 

To create and retrieve query runs, grant **Data** > **Queries** > **Read** (`data_queries_read`) on a [restricted API key](https://dashboard.stripe.com/apikeys).

To see the tables and columns you can query, [browse table schemas](https://docs.stripe.com/data/api/schemas.md).

## Get SQL to run 

Put a SQL string in `query.sql`. Sources that work well:

- A query you’ve already written or saved in the [Sigma query editor](https://docs.stripe.com/data/write-queries.md)
- A natural-language question to a coding agent through the [Stripe MCP server](https://docs.stripe.com/data/analyze-with-ai.md), such as “Total my refunds by month for this year”
- A `SELECT` against a table from [Browse table schemas](https://docs.stripe.com/data/api/schemas.md)

The example below queries `balance_transactions`. Swap in any table and columns your schema list returns.

## Create a QueryRun

Send a POST request with your SQL in `query.sql`. Stripe starts the query and returns a `QueryRun` immediately. The first response has `status=running` and `result=null` because the query hasn’t finished. Save the `id`, then [retrieve the QueryRun](https://docs.stripe.com/data/api/query-runs.md#retrieve-query-run) when it completes.

```curl
curl -X POST https://api.stripe.com/v2/data/query_runs \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-09-30.preview" \
  --json '{
    "query": {
        "sql": "SELECT * FROM balance_transactions LIMIT 10"
    },
    "dataset": "analytical",
    "format": "csv"
  }'
```

```json
{
  "id": "qrun_123",
  "object": "v2.data.query_run",
  "created": "2026-07-03T01:02:29.964Z",
  "query": {
    "sql": "SELECT * FROM balance_transactions LIMIT 10"
  },
  "dataset": "analytical",
  "format": "csv",
  "status": "running",
  "result": null,
  "result_options": {
    "compress_file": false
  },
  "livemode": false
}
```

Use the returned `id` to [retrieve the QueryRun](https://docs.stripe.com/data/api/query-runs.md#retrieve-query-run) after it completes.

## Retrieve a QueryRun

Retrieve the `QueryRun` using the `id` returned in the create response. If you poll, continue until `status` is no longer `running`. When `status` is `succeeded`, download the file from `result.file.download_url.url`. When `status` is `failed`, read [status_details](https://docs.stripe.com/data/api/query-runs.md#handle-a-failed-query). You can also [listen for webhooks](https://docs.stripe.com/data/api/query-runs.md#webhooks) instead of polling.

```curl
curl https://api.stripe.com/v2/data/query_runs/qrun_123 \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-09-30.preview"
```

```json
{
  "id": "qrun_123",
  "object": "v2.data.query_run",
  "created": "2026-07-03T01:02:29.964Z",
  "query": {
    "sql": "SELECT * FROM balance_transactions LIMIT 10"
  },
  "dataset": "analytical",
  "format": "csv",
  "status": "succeeded",
  "result": {
    "file": {
      "content_type": "csv",
      "download_url": {
        "expires_at": "2026-07-03T01:10:46.679Z",
        "url": "https://stripeusercontent.com/files/us-west-2/download/wksp_123/file_123/qrun_123.csv..."
      },
      "size": "512"
    }
  },
  "result_options": {
    "compress_file": false
  },
  "livemode": false
}
```

> The download URL expires after 5 minutes. Retrieve the `QueryRun` again to get a new URL.

## Page results inline

To read rows in the API response instead of downloading a file, retrieve the run with `include[0]=result.inline`. Use `limit` (default 100, maximum 1,000) and follow `next_page_url` to page through rows. Keep the same `limit` on every request in a pagination sequence. To use a different `limit`, start pagination again without a page token.

```curl
curl -G https://api.stripe.com/v2/data/query_runs/qrun_123 \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-09-30.preview" \
  -d "include[0]=result.inline" \
  -d limit=100
```

```json
{
  "id": "qrun_123",
  "object": "v2.data.query_run",
  "status": "succeeded",
  "result": {
    "inline": {
      "columns": [
        {"name": "id", "type": "varchar"},
        {"name": "amount", "type": "bigint"}
      ],
      "rows": [
        {
          "data": {
            "id": "txn_123",
            "amount": "1000"
          }
        }
      ],
      "next_page_url": null,
      "previous_page_url": null
    },
    "row_count": "1",
    "col_count": "2"
  },
  "livemode": false
}
```

## Understand cached runs 

We cache query runs to avoid repeating the same execution. A matching create request can return an existing `QueryRun` with the same `id` and `created` timestamp instead of creating another run.

A cache match requires all of the following:

- The same Stripe account or [organization](https://docs.stripe.com/get-started/account/orgs.md), and the same set of accounts you’re authorized to query.
- The same live mode or [sandbox](https://docs.stripe.com/sandboxes.md) context.
- The same SQL after query processing.

- The same `format` and `result_options.compress_file` settings.
- A run that’s still `running` or that `succeeded` less than seven days ago.
- The same run-level data freshness timestamp determined by Stripe.

Reusing a run doesn’t emit another `v2.data.query_run.created` event. If the returned run is already `succeeded`, use its results without waiting for another completion event.

## Webhooks 

Instead of polling, listen for `v2.data.query_run.succeeded` when the query succeeds or `v2.data.query_run.failed` when it fails. After receiving the event, retrieve the `QueryRun` and read the result URL or [status_details](https://docs.stripe.com/data/api/query-runs.md#handle-a-failed-query). Before processing these events, [verify incoming webhook signatures](https://docs.stripe.com/webhooks.md#verify-events) and review the Stripe [public IP addresses](https://docs.stripe.com/ips.md).

## Handle a failed query 

If a query fails after you create the run, the `QueryRun` remains available with `status` set to `failed`. Because the create request succeeded, the query failure doesn’t return an HTTP error. Retrieve the [QueryRun](https://docs.stripe.com/api/v2/data/query-runs/object.md) and read `status_details.code` and `status_details.message`.

If SQL validation fails before the run starts, no `QueryRun` is created. The request returns HTTP `400` with the code `query_run_invalid_sql`.

|  |
| `query_run_invalid_sql` | The SQL passed initial validation but failed during execution. The `message` describes the error, such as an unknown column. |
| `file_size_above_limit` | The result exceeds the 5 GB file limit. Narrow the query or create the run with `result_options.compress_file` set to `true`. |
| `internal_error` | The query reached a resource limit, ran for more than 90 minutes, or failed for another reason. The `message` describes the failure. |

```json
{
  "id": "qrun_123",
  "object": "v2.data.query_run",
  "status": "failed",
  "result": null,
  "status_details": {
    "code": "query_run_invalid_sql",
    "message": "The query referenced a column that doesn't exist: 'nonexistent_col'."
  }
}
```

## Request file compression 

For large result sets, set `result_options.compress_file` to `true` to receive a ZIP-compressed file.

```curl
curl -X POST https://api.stripe.com/v2/data/query_runs \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-09-30.preview" \
  --json '{
    "query": {
        "sql": "SELECT * FROM balance_transactions"
    },
    "dataset": "analytical",
    "format": "csv",
    "result_options": {
        "compress_file": true
    }
  }'
```

## Use an Organization API key 

The Query Run API accepts [Organization API keys](https://docs.stripe.com/keys/organization-api-keys.md).

### Run a query across your organization

Omit `Stripe-Context` to run in organization mode, equivalent to [Sigma for Organizations](https://docs.stripe.com/data/sigma-organizations.md). Your SQL can read data from every direct account, and an `account` column identifies the account for each row.

```curl
curl -X POST https://api.stripe.com/v2/data/query_runs \
  -H "Authorization: Bearer sk_org_123" \
  -H "Stripe-Version: 2026-09-30.preview" \
  --json '{
    "query": {
        "sql": "SELECT account, id, amount FROM balance_transactions LIMIT 10"
    },
    "dataset": "analytical",
    "format": "csv"
  }'
```

### Run a query scoped to a single account

Set the [Stripe-Context](https://docs.stripe.com/context.md) header to the account you want to query.

```curl
curl -X POST https://api.stripe.com/v2/data/query_runs \
  -H "Authorization: Bearer sk_org_123" \
  -H "Stripe-Version: 2026-09-30.preview" \
  -H "Stripe-Context: {{CONTEXT_ID}}" \
  --json '{
    "query": {
        "sql": "SELECT * FROM balance_transactions LIMIT 10"
    },
    "dataset": "analytical",
    "format": "csv"
  }'
```

## Limits 

For general details about APIs in the v2 namespace, see the [API v2 overview](https://docs.stripe.com/api-v2-overview.md). For general API rate limits, see [Rate limits](https://docs.stripe.com/rate-limits.md). Specific limits include:

|  |
| Concurrent query runs | 500 `QueryRun` objects can be `running` at the same time in live mode, and 100 in a *sandbox* (A sandbox is an isolated test environment that allows you to test Stripe functionality in your account without affecting your live integration. Use sandboxes to safely experiment with new features and changes), per account or organization. Requests that exceed the limit return a `429` status code. Wait for running queries to finish to free capacity. |
| Query execution timeout | Queries that run longer than 90 minutes fail, and the `QueryRun` moves to `status=failed`. |
| File size | 5 GB per result file. Request `result_options.compress_file=true` if you reach this limit. |
| File retention | Stripe retains query run results for 90 days. |
| Download URL lifetime | File download URLs expire after 5 minutes. |

## See also

- [Browse table schemas](https://docs.stripe.com/data/api/schemas.md)
- [Access Stripe data with the API](https://docs.stripe.com/data/api.md)
- [Run a report](https://docs.stripe.com/data/api/reports.md)
- [Write queries using Sigma](https://docs.stripe.com/data/write-queries.md)
- [Analyze your Stripe data with AI](https://docs.stripe.com/data/analyze-with-ai.md)
