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

# Exporting to Snowflake

> Sync your Roark call analysis into a data warehouse to join it against your own operational data

## Overview

Roark analyzes every call and produces structured metric values (frustration, sentiment, task completion, latency, custom metrics, and more). A common next step is to land that analysis in your own data warehouse so you can join it against your operational data, for example correlating call frustration with downstream business outcomes.

This guide covers the recommended pattern for syncing Roark analysis into **Snowflake** using the tools available today. The same approach works for BigQuery, Redshift, or Databricks: only the load step changes.

<Note>
  Want a fully managed connector or a scheduled file drop into your own bucket? See [Deeper integrations](#deeper-integrations) at the bottom of this page and [reach out](/documentation/resources/support). We are actively expanding warehouse-export options.
</Note>

***

## The recommended pattern

The pattern is **event-driven pull**: Roark notifies you when a call finishes analysis, you pull the results from the API, land the raw JSON in cloud storage, and load it into Snowflake.

```
Roark webhook  ──▶  your handler  ──▶  fetch metrics/transcript  ──▶  S3 (raw JSON)  ──▶  Snowpipe  ──▶  Snowflake
```

Roark webhooks are a **notification, not the payload**. When a call is analyzed you receive the `callId`, not the metric values themselves, so your handler makes one follow-up API call to fetch the analysis. This keeps payloads small and lets you pull exactly the fields you need.

<Steps>
  <Step title="Create a read-only API key">
    Generate a key scoped to the project you want to export. Read endpoints require the `call:read`, `metric:read`, and `transcript:read` permissions. See [API Keys](/documentation/getting-started/api-keys).
  </Step>

  <Step title="Subscribe to analysis webhooks">
    Add a webhook endpoint and subscribe to `call.analysis.completed`. See [Webhooks](/documentation/integrations/webhooks).
  </Step>

  <Step title="Fetch analysis on each event">
    In your handler, call the metrics (and optionally transcript) endpoints for the `callId` you received.
  </Step>

  <Step title="Land the raw JSON in cloud storage">
    Write each call's payload to S3 (or GCS / Azure Blob). Store it as raw JSON so schema changes never break ingestion.
  </Step>

  <Step title="Load into Snowflake">
    Point Snowpipe at the bucket to auto-ingest, then flatten into modeled tables.
  </Step>
</Steps>

***

## What you can export

All data is served from the Customer API at `https://api.roark.ai/v1` and is scoped to the project your API key belongs to.

| Data                                     | Endpoint                                               | Permission        |
| ---------------------------------------- | ------------------------------------------------------ | ----------------- |
| List of calls (paginated)                | `GET /v1/call`                                         | `call:read`       |
| Single call detail                       | `GET /v1/call/{callId}`                                | `call:read`       |
| Metric values for a call                 | `GET /v1/call/{callId}/metrics`                        | `metric:read`     |
| Transcript for a call                    | `GET /v1/call/{callId}/transcript`                     | `transcript:read` |
| Metric values for a whole collection job | `GET /v1/metric/collection-jobs/{jobId}/metric-values` | `metric:read`     |
| Metric definitions (catalog)             | `GET /v1/metric/definitions`                           | `metric:read`     |

Chats have the identical set of endpoints under `/v1/chat`.

<Tip>
  Every call carries an `externalId` (your own correlation ID, set when the call is ingested) and a `properties` map of custom key-values. These are your join keys back to operational data in your warehouse.
</Tip>

***

## Approach A: Event-driven (recommended for ongoing sync)

Best for keeping the warehouse continuously fresh with low latency.

<CodeGroup>
  ```python Python (webhook handler) theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
  @app.route('/webhooks/roark', methods=['POST'])
  def handle_webhook():
      payload = request.json
      if payload['event'] == 'call.analysis.completed':
          call_id = payload['data']['callId']

          # Pull the analysis Roark just finished
          call = get(f'/v1/call/{call_id}')
          metrics = get(f'/v1/call/{call_id}/metrics')

          # Land raw JSON in S3, keyed by call id
          s3.put_object(
              Bucket='my-roark-export',
              Key=f'calls/{call_id}.json',
              Body=json.dumps({'call': call, 'metrics': metrics}),
          )
      return '', 200


  def get(path):
      r = requests.get(
          f'https://api.roark.ai{path}',
          headers={'Authorization': f'Bearer {ROARK_API_KEY}'},
          timeout=10,
      )
      r.raise_for_status()
      return r.json()
  ```

  ```typescript TypeScript (webhook handler) theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
  app.post('/webhooks/roark', async (req, res) => {
    const { event, data } = req.body
    if (event === 'call.analysis.completed') {
      const callId = data.callId

      const call = await get(`/v1/call/${callId}`)
      const metrics = await get(`/v1/call/${callId}/metrics`)

      await s3.putObject({
        Bucket: 'my-roark-export',
        Key: `calls/${callId}.json`,
        Body: JSON.stringify({ call, metrics }),
      })
    }
    res.sendStatus(200)
  })

  const get = async (path: string) => {
    const r = await fetch(`https://api.roark.ai${path}`, {
      headers: { Authorization: `Bearer ${process.env.ROARK_API_KEY}` },
    })
    if (!r.ok) throw new Error(`${path} -> ${r.status}`)
    return r.json()
  }
  ```
</CodeGroup>

***

## Approach B: Scheduled backfill / poll

Best for the initial backfill of historical data, or as a simpler alternative to webhooks. Page through the calls list on a schedule and fetch metrics per call.

The list endpoint uses keyset (cursor) pagination. Sort by `startedAt` and walk the cursor to move forward in time.

<CodeGroup>
  ```python Python (paginated pull) theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
  def export_all_calls():
      cursor = None
      while True:
          params = {'limit': 100, 'sortBy': 'startedAt', 'sortDirection': 'asc'}
          if cursor:
              params['after'] = cursor

          page = get('/v1/call', params=params)
          for call in page['data']:
              metrics = get(f"/v1/call/{call['id']}/metrics")
              write_to_s3(call, metrics)

          if not page['hasMore']:
              break
          cursor = page['nextCursor']
  ```

  ```bash cURL (single page) theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
  curl -G "https://api.roark.ai/v1/call" \
    -H "Authorization: Bearer $ROARK_API_KEY" \
    --data-urlencode "limit=100" \
    --data-urlencode "sortBy=startedAt" \
    --data-urlencode "sortDirection=asc" \
    --data-urlencode "after=$CURSOR"
  ```
</CodeGroup>

<Note>
  For a large backfill you can also pull metric values a whole job at a time with `GET /v1/metric/collection-jobs/{jobId}/metric-values`, which returns every metric value for that collection job in one paginated stream rather than one request per call.
</Note>

***

## Loading into Snowflake

Land the raw JSON in an external stage, ingest into a `VARIANT` staging table, then flatten into modeled tables with `LATERAL FLATTEN`. Storing the raw payload first means a new metric or field never breaks your pipeline.

<Steps>
  <Step title="Create an external stage over your bucket">
    ```sql theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
    create or replace stage roark_stage
      url = 's3://my-roark-export/calls/'
      storage_integration = my_s3_integration
      file_format = (type = json);
    ```
  </Step>

  <Step title="Auto-ingest with Snowpipe">
    ```sql theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
    create table roark_raw (raw variant, loaded_at timestamp_ntz default current_timestamp());

    create or replace pipe roark_pipe auto_ingest = true as
      copy into roark_raw (raw) from @roark_stage;
    ```
  </Step>

  <Step title="Flatten metrics into a queryable table">
    ```sql theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
    create or replace view roark_call_metrics as
    select
      raw:call:id::string           as call_id,
      raw:call:externalId::string   as external_id,   -- your join key
      raw:call:startedAt::timestamp as started_at,
      raw:call:durationMs::number   as duration_ms,
      raw:call:callDirection::string as direction,
      m.value:slug::string          as metric_slug,
      m.value:name::string          as metric_name,
      m.value:type::string          as metric_type,
      m.value:value::variant        as metric_value,
      m.value:captureStatus::string as capture_status
    from roark_raw,
      lateral flatten(input => raw:metrics:data) m;
    ```
  </Step>

  <Step title="Join against your operational data">
    ```sql theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
    -- Does call frustration predict whether a caregiver takes a shift?
    select
      f.metric_value::float as frustration,
      s.shift_accepted
    from roark_call_metrics f
    join shifts s
      on s.roark_external_id = f.external_id
    where f.metric_slug = 'frustration';
    ```
  </Step>
</Steps>

<Tip>
  Model the flattened view once per metric type you care about, or keep one long-format `(call_id, metric_slug, metric_value)` table and pivot in your BI layer. Long format handles new metrics with zero schema changes.
</Tip>

***

## Things to know

* **Idempotency.** Keying S3 objects and Snowflake rows by `callId` (plus the metric collection job id) makes re-delivery and backfills safe to re-run without double counting.
* **Webhook delivery.** Roark retries failed deliveries up to 3 times with backoff. Respond `200` within 5 seconds and process asynchronously. See [Webhooks](/documentation/integrations/webhooks).
* **Date filtering.** The calls list does not yet accept an explicit `from`/`to` date range. Approximate a range by sorting on `startedAt` and walking the cursor until you pass your window.
* **Transcripts** are fetched per call from the transcript endpoint; they are not included in the metrics payload.
* **Rate limits and pagination.** Use `limit=100` and honor `nextCursor`/`hasMore` rather than assuming a fixed page count.

***

## Deeper integrations

The pattern above uses only what is available today. If you need something more managed, we are building toward it and would like to scope it with you:

<CardGroup cols={2}>
  <Card title="Managed connector" icon="plug">
    A Fivetran / Airbyte-style connector with incremental, updated-since cursors so your warehouse stays in sync automatically.
  </Card>

  <Card title="Scheduled file drop" icon="folder-tree">
    Roark writes incremental Parquet or CSV to your own S3 bucket on a schedule, ready for Snowpipe.
  </Card>

  <Card title="Secure data share" icon="share-2">
    A hands-off Snowflake Secure Data Share so there is nothing to run on your side.
  </Card>

  <Card title="Talk to us" icon="life-ring" href="/documentation/resources/support">
    Tell us your volume, latency needs, and the joins you want to run.
  </Card>
</CardGroup>

***

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/documentation/integrations/webhooks">
    Set up the analysis-completed notification
  </Card>

  <Card title="API Keys" icon="code" href="/documentation/getting-started/api-keys">
    Create a read-only key for the export
  </Card>

  <Card title="API Reference" icon="terminal" href="/api-reference/introduction">
    Full endpoint and parameter reference
  </Card>

  <Card title="Reports" icon="file-text" href="/documentation/observability/reports">
    Analyze the same data inside Roark
  </Card>
</CardGroup>
