> ## Documentation Index
> Fetch the complete documentation index at: https://docs.querying.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Result exports to Amazon S3, Google Cloud Storage and BigQuery

> Receive every finished task and scored monitor answer as hourly or daily JSON Lines files in your own bucket, or as BigQuery table partitions.

Each hour or each UTC day, an export writes the window's results as gzip-compressed JSON Lines to
an Amazon S3 or Google Cloud Storage bucket, or loads them into a BigQuery table partition. Your
warehouse reads them like any other source, with no polling loop.

You grant our export identity write access to the bucket or dataset; you never share keys
or passwords. Every key, bucket, account id and role on this page is a placeholder.

## What is exported

| Data set | Rows | Window by | Same fields as |
| - | - | - | - |
| `tasks` | Every task of the account that finished as `COMPLETED` or `FAILED` | `completedAt` | [`GET /v1/async/task/{id}`](/api-reference/get-task) and the webhook |
| `monitor_results` | Every scored answer of every monitor of the account | `ranAt` | [`GET /v1/monitors/{id}/results?includeEvidence=true`](/api-reference/monitors/export-results) |

Tasks submitted with any of the account's keys, from the Playground and by monitors are all
included. A window holds exactly the rows whose time falls in `[windowStart, windowEnd)`.

**Delivery timing.** A window is due 10 minutes after it closes, so charges settled at the
boundary are in it. Due windows are picked up by a job that runs every 15 minutes, so
delivery normally starts 10 to 25 minutes after the window closes.

## Set up an export

**1. Read your export identity**

```bash theme={"theme":"css-variables"}
curl https://api.querying.ai/v1/exports/identity \
  -H "Authorization: Bearer $QUERYING_API_KEY"
```

```json theme={"theme":"css-variables"}
{
  "success": true,
  "identity": {
    "accountKey": "acct_3f9a1c2b7d4e",
    "gcp": { "serviceAccount": "querying-export@example-project.iam.gserviceaccount.com" },
    "aws": {
      "federatedPrincipal": "accounts.google.com",
      "audience": "117000000000000000001",
      "oaud": "querying-result-export",
      "roleSessionName": "querying-acct_3f9a1c2b7d4e",
      "trustPolicy": { "Version": "2012-10-17", "Statement": ["..."] }
    }
  }
}
```

`accountKey` names your folder and your tables. Every path and table we write carries
it, so several querying.ai accounts can share one bucket or dataset and each writes
only its own files. Grant the identity your response shows, not the placeholder above.

**2. Grant access**

Grant the identity access to your destination. See
[Grant access](#grant-access) for S3, Cloud Storage and BigQuery.

**3. Create the export**

`POST /v1/exports` checks the grant and saves the export. See
[Create the export](#create-the-export).

**4. Watch the first delivery**

`GET /v1/exports/{id}/runs` lists each window and its status. See
[Delivery history](#delivery-history).

## Grant access

### Amazon S3

Create an IAM role in your AWS account that our Google identity can assume. Use the
`trustPolicy` from the identity response as the role's trust policy. It accepts only our
export identity, and only with your account's session name:

```json theme={"theme":"css-variables"}
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Federated": "accounts.google.com" },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": { "StringEquals": {
      "accounts.google.com:aud": "117000000000000000001",
      "accounts.google.com:oaud": "querying-result-export",
      "sts:RoleSessionName": "querying-acct_3f9a1c2b7d4e"
    } }
  }]
}
```

Attach a permissions policy that lets the role write under your prefix:

```json theme={"theme":"css-variables"}
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "s3:PutObject",
    "Resource": "arn:aws:s3:::acme-geo/querying/*"
  }]
}
```

If the bucket encrypts with a customer-managed KMS key, also allow `kms:GenerateDataKey` on
that key.

### Google Cloud Storage

Grant the export service account **Storage Object User** (`roles/storage.objectUser`) on
the bucket. It creates the files and replaces them when a window is sent again.

```bash theme={"theme":"css-variables"}
gcloud storage buckets add-iam-policy-binding gs://acme-geo \
  --member="serviceAccount:querying-export@example-project.iam.gserviceaccount.com" \
  --role="roles/storage.objectUser"
```

To limit the grant to your folder, add an IAM condition on
`resource.name.startsWith("projects/_/buckets/acme-geo/objects/querying/acct_3f9a1c2b7d4e/")`.

### BigQuery

Grant the export service account **BigQuery Data Editor** (`roles/bigquery.dataEditor`) on
the dataset. We run the load jobs in our own project, so loading costs you nothing; you pay
only for storage and the queries you run.

```bash theme={"theme":"css-variables"}
bq add-iam-policy-binding \
  --member="serviceAccount:querying-export@example-project.iam.gserviceaccount.com" \
  --role="roles/bigquery.dataEditor" acme-analytics:geo_raw
```

## Create the export

Use an API key without an engine scope: an export carries every result of the account, so a
key limited to some engines gets `403 KEY_SCOPE`.

```bash Amazon S3 theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/exports \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination": {
      "type": "s3",
      "bucket": "acme-geo",
      "region": "us-east-1",
      "roleArn": "arn:aws:iam::123456789012:role/querying-export",
      "prefix": "querying"
    },
    "interval": "daily",
    "data": ["tasks", "monitor_results"]
  }'
```

```bash Google Cloud Storage theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/exports \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination": { "type": "gcs", "bucket": "acme-geo", "prefix": "querying" },
    "interval": "hourly",
    "data": ["tasks"]
  }'
```

```bash BigQuery theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/exports \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination": { "type": "bigquery", "projectId": "acme-analytics", "datasetId": "geo_raw" },
    "interval": "daily",
    "startAt": "2026-10-01T00:00:00Z"
  }'
```

| Field | Required | Description |
| - | - | - |
| `destination.type` | Yes | `s3`, `gcs` or `bigquery`. |
| `destination.bucket` | S3, GCS | The bucket name. |
| `destination.prefix` | No | Folder inside the bucket, up to 256 characters, relative (no leading `/`, no `.` or `..` segments). A trailing `/` is added. |
| `destination.region` | S3 | The bucket's AWS region, such as `us-east-1`. |
| `destination.roleArn` | S3 | The role from [Grant access](#amazon-s3), `arn:aws:iam::<account>:role/<name>`. |
| `destination.projectId`, `destination.datasetId` | BigQuery | The Google Cloud project and dataset. |
| `interval` | No | `hourly` or `daily` (default). Fixed for the life of the export. |
| `data` | No | `tasks`, `monitor_results` or both (default). |
| `startAt` | No | ISO 8601 time, not in the future and at most 30 days back. The first window is the one containing it. Default: now. |

Before saving, the export proves the grant. For S3 and Cloud Storage it writes
`<prefix><accountKey>/_querying/connection-check.json`; for BigQuery it reads the dataset
and records its location. If the check fails, nothing is saved. An account can have up to 10
exports.

The response is `201` with the export:

```json theme={"theme":"css-variables"}
{
  "success": true,
  "export": {
    "id": "6a1d0f3c-...",
    "destination": {
      "type": "bigquery",
      "projectId": "acme-analytics",
      "datasetId": "geo_raw",
      "location": "US",
      "tables": [
        "acme-analytics.geo_raw.querying_tasks_v1_acct_3f9a1c2b7d4e",
        "acme-analytics.geo_raw.querying_monitor_results_v1_acct_3f9a1c2b7d4e"
      ]
    },
    "interval": "daily",
    "data": ["tasks", "monitor_results"],
    "enabled": true,
    "schemaVersion": 1,
    "nextWindowStart": "2026-10-01T00:00:00.000Z",
    "createdAt": "2026-10-19T09:00:00.000Z",
    "deliveredThrough": null,
    "failedRuns": 0,
    "retryingRuns": 0
  }
}
```

For S3 and Cloud Storage, `destination` echoes the bucket settings and adds `path`, the
folder your files land in, such as `s3://acme-geo/querying/acct_3f9a1c2b7d4e/` or
`gs://acme-geo/querying/acct_3f9a1c2b7d4e/`. `nextWindowStart` is the start of the next
window to be planned.

### Errors

| Status | Code | Cause |
| - | - | - |
| `400` | `VALIDATION_ERROR` | Invalid JSON or a field out of range. The message names the field, such as `destination.region: region is the bucket's AWS region, e.g. us-east-1`. |
| `403` | `KEY_SCOPE` | The API key is limited to some engines. |
| `409` | `EXPORT_LIMIT` | The account already has 10 exports. |
| `422` | `EXPORT_DESTINATION_UNREACHABLE` | The connection check failed. The message carries the storage provider's reason, usually a missing grant. |
| `502` | `EXPORT_CHECK_FAILED` | The destination could not be reached. Try again. |
| `503` | `EXPORTS_NOT_CONFIGURED` | Exports to this destination type are not available. |

## Files and tables

### Files in S3 and Cloud Storage

```text theme={"theme":"css-variables"}
s3://acme-geo/querying/acct_3f9a1c2b7d4e/tasks/v1/dt=2026-10-08/part-00000.jsonl.gz
s3://acme-geo/querying/acct_3f9a1c2b7d4e/tasks/v1/dt=2026-10-08/_manifest.json
s3://acme-geo/querying/acct_3f9a1c2b7d4e/monitor_results/v1/dt=2026-10-08/hour=13/part-00000.jsonl.gz
```

* Directories follow Hive partitioning (`dt=`, and `hour=` for hourly exports), so Athena,
  BigQuery external tables and Spark read them as columns.
* `v1` is the schema version (`schemaVersion` on the export). A change that breaks a column
  ships as `v2` beside it.
* A window splits into parts of up to 100,000 rows, in time order.
* `_manifest.json` is written last. Load a window once its manifest exists, and read the
  part list from it:

```json theme={"theme":"css-variables"}
{
  "schemaVersion": 1,
  "data": "tasks",
  "windowStart": "2026-10-08T00:00:00.000Z",
  "windowEnd": "2026-10-09T00:00:00.000Z",
  "rows": 20000,
  "files": [{ "key": "querying/acct_3f9a1c2b7d4e/tasks/v1/dt=2026-10-08/part-00000.jsonl.gz", "rows": 20000, "bytes": 1990096 }],
  "deliveredAt": "2026-10-09T00:16:02.114Z"
}
```

`rows` in the manifest equals the number of lines in its parts and the `rows` of the run in
the delivery history. File names depend only on the window, so a re-sent window replaces the
same objects.

### BigQuery tables

BigQuery exports load into `querying_tasks_v1_<accountKey>` and
`querying_monitor_results_v1_<accountKey>`, created on the first load and partitioned by
`completedAt` or `ranAt` (by day, or by hour for hourly exports). Each window replaces its
own partition, so a re-sent window never doubles. `payload`, `response`, `sources` and
`scoringContext` are `JSON` columns:

```sql theme={"theme":"css-variables"}
SELECT taskType, JSON_VALUE(payload.prompt) AS prompt, JSON_VALUE(response.text) AS answer
FROM `acme-analytics.geo_raw.querying_tasks_v1_acct_3f9a1c2b7d4e`
WHERE DATE(completedAt) = "2026-10-08" AND status = "COMPLETED";
```

## Row schema (v1)

### `tasks`

| Field | Type | BigQuery type | Description |
| - | - | - | - |
| `id` | string | `STRING` | Task id. |
| `taskType` | string | `STRING` | Engine, such as `CHATGPT`. |
| `status` | string | `STRING` | `COMPLETED` or `FAILED`. |
| `priority` | integer | `INT64` | As submitted. |
| `idempotencyKey` | string or null | `STRING` | Your key, when you sent one. |
| `createdAt`, `completedAt` | timestamp | `TIMESTAMP` | UTC, microsecond precision. |
| `monitorId` | string or null | `STRING` | The monitor that ran it. |
| `creditsCharged` | decimal | `NUMERIC` | Credits charged for the task, with up to two decimals (for example `1.25`). `0` for a failed task. Read it as a decimal, not an integer. |
| `payload` | object | `JSON` | The payload as submitted. |
| `response` | object or null | `JSON` | The result as `GET /v1/async/task/{id}` returns it; `null` when the task failed. |
| `error` | string or null | `STRING` | The failure reason, starting with its code; `null` when completed. |

### `monitor_results`

| Field | Type | BigQuery type | Description |
| - | - | - | - |
| `id`, `monitorId`, `taskId` | string | `STRING` | Result, monitor and task ids. |
| `engine`, `prompt` | string | `STRING` | What was asked where. |
| `mentioned`, `cited` | boolean | `BOOL` | Brand named; your domain cited. |
| `position` | integer or null | `INT64` | Character offset of the first brand mention. |
| `competitorHits` | string array | `STRING`, repeated | Competitors named in the answer. |
| `sources` | array | `JSON` | Citations as `{url, label, position}`. |
| `answerText` | string or null | `STRING` | The answer the score was read from. |
| `scoringContext` | object or null | `JSON` | The scoring definition at the time. |
| `latitude`, `longitude` | number or null | `FLOAT64` | The point asked from, for location monitors. |
| `ranAt` | timestamp | `TIMESTAMP` | UTC, microsecond precision. |

## Delivery history

Each window of each data set is one run. List them newest first:

```bash theme={"theme":"css-variables"}
curl "https://api.querying.ai/v1/exports/$EXPORT_ID/runs?status=failed" \
  -H "Authorization: Bearer $QUERYING_API_KEY"
```

```json theme={"theme":"css-variables"}
{
  "success": true,
  "runs": [{
    "id": "3c9e1b7a-...",
    "data": "tasks",
    "windowStart": "2026-10-08T00:00:00.000Z",
    "windowEnd": "2026-10-09T00:00:00.000Z",
    "status": "failed",
    "attempts": 8,
    "nextAttemptAt": null,
    "rows": null,
    "bytes": null,
    "files": [],
    "error": "S3 PutObject s3://acme-geo/... failed: HTTP 403 AccessDenied: Access Denied",
    "deliveredAt": null
  }],
  "nextCursor": null
}
```

| `status` | Meaning |
| - | - |
| `pending` | Waiting to be delivered, or waiting to retry after a failed attempt. `nextAttemptAt` says when. |
| `running` | Being delivered now. |
| `delivered` | Written. `rows`, `bytes`, `files` and `deliveredAt` are set. |
| `failed` | Eight attempts failed. Nothing more happens until you retry it. |

Filter with `status`, `data` (`tasks` or `monitor_results`) and `limit` (1–200, default
50\); pass `nextCursor` back as `cursor` for the next page.

* **Automatic retries.** A failed attempt is retried after 15 minutes, then with the delay
  doubling up to 12 hours, eight attempts in all. `error` holds the storage provider's
  reason, so a missing permission shows up here first.
* **Send a window again.** `POST /v1/exports/{id}/runs/{runId}/retry` sends a window now: a
  failed one after you fix the grant, or a delivered one you want rewritten. It resets
  `attempts` and returns the run as `pending`. A run that is being delivered returns
  `409 EXPORT_RUN_RUNNING`.
* **Export health.** `GET /v1/exports` and `GET /v1/exports/{id}` add `deliveredThrough`
  (the end of the latest delivered window), `failedRuns` and `retryingRuns` (pending runs
  that already failed at least once).

## Pause, resume and delete

* `PATCH /v1/exports/{id}` with `{"enabled": false}` pauses an export. `{"enabled": true}`
  resumes it and catches up on the windows it missed, back to 30 days.
* `DELETE /v1/exports/{id}` stops the export. Files and partitions already delivered stay in
  your storage.
* Destination, interval and data sets cannot change. Create a new export and delete the
  old one.

## Endpoints

| Method and path | Purpose |
| - | - |
| [`GET /v1/exports/identity`](/api-reference/exports/get-identity) | The identity to grant and your account key |
| [`POST /v1/exports`](/api-reference/exports/create-export) | Connect a destination |
| [`GET /v1/exports`](/api-reference/exports/list-exports) | List exports with delivery health |
| [`GET /v1/exports/{id}`](/api-reference/exports/get-export) | One export |
| [`PATCH /v1/exports/{id}`](/api-reference/exports/update-export) | Pause or resume |
| [`DELETE /v1/exports/{id}`](/api-reference/exports/delete-export) | Stop the export |
| [`GET /v1/exports/{id}/runs`](/api-reference/exports/list-runs) | Delivery history |
| [`POST /v1/exports/{id}/runs/{runId}/retry`](/api-reference/exports/retry-run) | Send a window again |

An export you do not own returns `404 NOT_FOUND`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.