# 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. A new run has `status=running` and `result=null`. If an identical run already exists, Stripe returns that run instead (see [Understand cached runs](https://docs.stripe.com/data/api/query-runs.md#cached-runs)). The returned run might already have `status=succeeded` with a result you can use right away. Check `status` in the create response before you poll or wait for a webhook. 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 `succeeded`, `failed`, or `canceled`. Stripe might add new statuses, so don’t assume this set of values is fixed. 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.

Stripe retains a `QueryRun` and its results for 90 days after the run completes. After 90 days, the results expire and attempts to retrieve the `QueryRun` return a `404` error. To regenerate the results, create a new `QueryRun`. To retain results for longer than 90 days, download and store them.

## 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 text. Stripe compares your SQL exactly as submitted and doesn’t normalize it. Changes to capitalization, comments, literal values, or whitespace create a new run.

- The same `format` and `result_options.compress_file` settings.
- The same `refreshed_at` value. This value indicates when Stripe last refreshed the data used by the run. After Stripe refreshes your data, the next matching request creates a new run with the updated data.
- A run that’s still `running` or that `succeeded` less than seven days ago.

The returned run is either `succeeded` or still `running` because an identical run is in progress.

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 a `QueryRun` and its results for 90 days after the run completes. After 90 days, the results expire. To regenerate the results, create a new `QueryRun`. |
| 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)
