# How to Integrate Bolt.new with Looker

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: April 2026

## TL;DR

Connect Bolt.new to Looker's REST API by creating API credentials in the Looker Admin panel, exchanging your client_id and client_secret for an access token via an API route, and then running Looks, dashboards, and inline queries server-side. Embed Looker dashboards using SSO embed URLs generated on the server. Looker requires a Google Cloud or Looker-licensed instance — it is not a free service.

## Embed Looker Analytics and Run API Queries from Bolt.new

Looker is Google Cloud's flagship business intelligence platform, built around LookML — a SQL-based data modeling language that lets data teams define metrics, dimensions, and relationships once and reuse them across reports. Looker's REST API is extensive: you can run any Explore, Look, or Dashboard programmatically, retrieve results as JSON, CSV, or Excel, manage users and groups, and generate signed SSO embed URLs that let you embed Looker's full interactive UI inside your own application. This makes Looker one of the most API-capable BI tools available, despite being an enterprise product. The query that surfaces users to this page — 'authorization: token looker api' — shows that developers are actively attempting the integration and running into the auth pattern.

The key technical detail to understand is Looker's auth flow. Unlike APIs that issue static API keys, Looker uses OAuth 2.0 client credentials: you POST your client_id and client_secret to Looker's /login endpoint, receive a short-lived access_token (valid for one hour), and include that token as a Bearer token in subsequent requests. Because your client_secret must never be exposed in the browser, every Looker API call must happen in a server-side context — a Next.js API route in your Bolt.new app. This API route fetches the token, makes the Looker API call, and returns the results to your frontend component. Caching the access token (until it expires) saves a round-trip on every request.

Looker is not a free or self-hosted product available to the general public. You need either a Looker instance on Google Cloud (billed through GCP), a Looker-licensed deployment through your organization, or access via a company that already uses Looker. If your organization uses Looker, your admin can provide API credentials. This tutorial assumes you have access to a Looker instance and focuses on the integration pattern.

## Before you start

- Access to a Looker instance (Google Cloud Looker, Looker-licensed deployment, or organizational Looker access)
- Admin access to the Looker instance to create API credentials (or have an admin create them for you)
- A Bolt.new project using Next.js (required for server-side API routes that keep the client_secret secure)
- Your Looker instance's base URL (e.g., https://yourcompany.looker.com) and API version (usually 4.0)
- For SSO embedding: the Looker embed secret, found in Admin → Platform → Embed in the Looker UI

## Step-by-step guide

### 1. Create Looker API Credentials

Looker API access requires API credentials tied to a Looker user account — either your own account or a dedicated service account. Log in to your Looker instance and navigate to Admin → Users (or your own profile if you are not an admin). Find the user you want to create API keys for and click 'Edit'. Scroll down to the 'API Keys' section and click 'New API Key'. Looker generates a client_id and client_secret pair. Copy both values immediately — the client_secret is shown only once and cannot be retrieved after leaving this page. If you lose it, you must generate a new key pair. The client_id is safe to identify your integration but the client_secret must be treated like a password. Store both in your .env file as LOOKER_CLIENT_ID and LOOKER_CLIENT_SECRET. Also note your Looker instance's base URL (e.g., https://yourcompany.looker.com) — you will need this for every API call. The Looker API base path is typically https://yourcompany.looker.com:19999/api/4.0 for Looker-licensed instances, or https://yourcompany.looker.com/api/4.0 for Looker on Google Cloud. If you are not a Looker admin and cannot access the Admin → Users panel, ask your Looker administrator to create an API key for you or for a dedicated service user with the permissions your app requires.

```
// lib/looker.ts
let cachedToken: string | null = null;
let tokenExpiresAt: number = 0;

export async function getLookerToken(): Promise<string> {
  // Return cached token if still valid (with 60s buffer)
  if (cachedToken && Date.now() < tokenExpiresAt - 60_000) {
    return cachedToken;
  }

  const baseUrl = process.env.LOOKER_BASE_URL;
  const response = await fetch(`${baseUrl}/api/4.0/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: process.env.LOOKER_CLIENT_ID!,
      client_secret: process.env.LOOKER_CLIENT_SECRET!,
    }),
  });

  if (!response.ok) {
    throw new Error(`Looker auth failed: ${response.status} ${await response.text()}`);
  }

  const data = await response.json();
  cachedToken = data.access_token;
  tokenExpiresAt = Date.now() + data.token_ttl * 1000;
  return cachedToken!;
}
```

**Expected result:** The getLookerToken() function successfully authenticates with Looker and returns a valid Bearer token. The token is cached and reused until near expiry.

### 2. Build an API Route to Fetch Look Results

With authentication handled in lib/looker.ts, create a Next.js API route that fetches the results of a specific Looker Look (a saved query). The Looker API endpoint for running a Look is GET /looks/:look_id/run/:result_format — where result_format can be json, json_detail, csv, or xlsx. Using json returns an array of row objects where each key is a 'view_name.field_name' Looker field reference. For example, a Look on the orders model might return rows like { 'orders.count': 1500, 'orders.average_revenue': 250.00 }. Your API route fetches the token, calls the Looker run endpoint with the Bearer token, and returns the data to your frontend component. Add error handling for common Looker API errors: 404 (Look not found or permission denied), 422 (Look has an error in its definition), and 401 (token expired — clear the cache and retry). In Bolt's WebContainer during development, outbound API calls to your Looker instance work correctly as long as your Looker instance allows requests from your IP. Note that Bolt's WebContainer does not support incoming webhook connections — but Looker's API is purely outbound (your app calls Looker, not the other way around), so this limitation does not apply to this integration.

```
// app/api/looker/look/[lookId]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getLookerToken } from '@/lib/looker';

export async function GET(
  request: NextRequest,
  { params }: { params: { lookId: string } }
) {
  const { lookId } = params;
  const baseUrl = process.env.LOOKER_BASE_URL;

  try {
    const token = await getLookerToken();
    const response = await fetch(
      `${baseUrl}/api/4.0/looks/${lookId}/run/json`,
      {
        headers: {
          'Authorization': `token ${token}`,
          'Content-Type': 'application/json',
        },
      }
    );

    if (response.status === 401) {
      // Token expired — this shouldn't happen with caching, but handle it
      return NextResponse.json(
        { error: 'Looker authentication expired. Please retry.' },
        { status: 401 }
      );
    }

    if (!response.ok) {
      const text = await response.text();
      return NextResponse.json(
        { error: `Looker API error: ${response.status}`, detail: text },
        { status: response.status }
      );
    }

    const data = await response.json();
    return NextResponse.json({ data, count: data.length });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch Look results' },
      { status: 500 }
    );
  }
}
```

**Expected result:** Calling /api/looker/look/123 returns the JSON results of Looker Look #123. The data array can be passed directly to a Recharts component or displayed in a table.

### 3. Generate SSO Embed URLs for Dashboard Embedding

Looker's SSO embed feature lets you embed a full interactive Looker dashboard in an iframe inside your app, without requiring your users to have Looker accounts. The embed URL is cryptographically signed using your Looker embed secret — a separate credential from your API keys, found in Admin → Platform → Embed. The signing process creates a URL that includes the dashboard path, user identity parameters (external_user_id, permissions, models), an expiry timestamp, and a signature hash computed using HMAC-SHA1. Because the signature involves your embed_secret, the URL must be generated server-side — never in the browser. The signed URL is valid for a short window (typically 5 minutes) and can only be used once. Your API route generates the URL and returns it to the frontend, which sets it as the src of an iframe. The Looker documentation provides detailed pseudocode for the signing algorithm — implement it carefully, as a single field in the wrong order breaks the signature. The nonce (random string in the URL) must be unique per request to prevent replay attacks. Looker's embed URL format has specific quoting and encoding requirements — test with Looker's embed URL validator if available in your Admin panel.

```
// app/api/looker/embed-url/route.ts
import { NextRequest, NextResponse } from 'next/server';
import * as crypto from 'crypto';

function sign(stringToSign: string, secret: string): string {
  return crypto.createHmac('sha1', secret).update(stringToSign).digest('hex');
}

export async function POST(request: NextRequest) {
  const { dashboardId, externalUserId, userDisplayName } = await request.json();
  const secret = process.env.LOOKER_EMBED_SECRET!;
  const baseUrl = process.env.LOOKER_BASE_URL!;
  const host = new URL(baseUrl).host;

  const nonce = crypto.randomBytes(16).toString('hex');
  const time = Math.floor(Date.now() / 1000);
  const sessionLength = 3600; // 1 hour
  const embedPath = `/embed/dashboards/${dashboardId}`;

  const permissions = JSON.stringify(['access_data', 'see_looks', 'see_user_dashboards']);
  const models = JSON.stringify(['all']); // Or specific model names
  const userAttributes = JSON.stringify({});
  const accessFilters = JSON.stringify({});

  const stringToSign = [
    host,
    embedPath,
    nonce,
    time,
    sessionLength,
    externalUserId,
    permissions,
    models,
    userAttributes,
    accessFilters,
    userDisplayName,
    '', // group_ids
    '', // external_group_id
  ].join('\n');

  const signature = sign(stringToSign, secret);

  const params = new URLSearchParams({
    nonce,
    time: time.toString(),
    session_length: sessionLength.toString(),
    external_user_id: externalUserId,
    permissions,
    models,
    user_attributes: userAttributes,
    access_filters: accessFilters,
    first_name: userDisplayName.split(' ')[0] || userDisplayName,
    last_name: userDisplayName.split(' ')[1] || '',
    force_logout_login: 'true',
    signature,
  });

  const embedUrl = `${baseUrl}/login/embed/${encodeURIComponent(embedPath)}?${params.toString()}`;
  return NextResponse.json({ embedUrl });
}
```

**Expected result:** The API route returns a signed Looker SSO embed URL. Setting this URL as an iframe's src displays the full interactive Looker dashboard inside your Bolt.new app without requiring users to have Looker accounts.

### 4. Display Looker Data in Custom React Charts

Instead of embedding Looker's iframe, you may want to display Look query results in your own custom React components — giving you full control over styling, layout, and interactivity. Fetch Look data via your /api/looker/look/:lookId route, then pass the results to Recharts components. Looker returns JSON where each row is an object with field names as keys in 'view.field' format. Map these to chart-friendly data structures in your component. For a bar chart showing monthly revenue, Looker might return rows like [{ 'orders.month': '2025-01', 'orders.total_revenue': 125000 }, ...] — transform these into [{ month: 'Jan 2025', revenue: 125000 }, ...] before passing to Recharts. Build a reusable LookerChart component that accepts a lookId prop, fetches the data, and renders the appropriate chart type. Add loading skeletons and error states. For tables, use a generic DataTable component that dynamically generates columns from the Looker field names, with the option to pass a column label mapping object for human-readable headers. During development in Bolt's WebContainer, API calls to Looker work as long as your Looker instance is accessible over the internet — corporate Looker instances behind a VPN cannot be reached from the Bolt preview environment.

```
// components/LookerChart.tsx
import { useState, useEffect } from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';

interface LookerBarChartProps {
  lookId: string;
  xField: string;
  yField: string;
  xLabel?: string;
  yLabel?: string;
}

export function LookerBarChart({ lookId, xField, yField, xLabel, yLabel }: LookerBarChartProps) {
  const [data, setData] = useState<Record<string, unknown>[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(`/api/looker/look/${lookId}`)
      .then(res => res.json())
      .then(({ data: rawData, error }) => {
        if (error) { setError(error); return; }
        // Normalize Looker field names to simple keys
        const normalized = rawData.map((row: Record<string, unknown>) => ({
          x: row[xField],
          y: Number(row[yField]) || 0,
        }));
        setData(normalized);
      })
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  }, [lookId, xField, yField]);

  if (loading) return <div className="h-64 bg-gray-100 animate-pulse rounded" />;
  if (error) return <div className="text-red-500">Error loading chart: {error}</div>;

  return (
    <ResponsiveContainer width="100%" height={300}>
      <BarChart data={data}>
        <CartesianGrid strokeDasharray="3 3" />
        <XAxis dataKey="x" label={{ value: xLabel, position: 'insideBottom', offset: -5 }} />
        <YAxis label={{ value: yLabel, angle: -90, position: 'insideLeft' }} />
        <Tooltip />
        <Bar dataKey="y" fill="#3b82f6" />
      </BarChart>
    </ResponsiveContainer>
  );
}
```

**Expected result:** Looker Look data displays in a Recharts bar chart and sortable table inside your Bolt.new app. The data comes from Looker's governed data model but is rendered in your custom UI components.

## Best practices

- Cache the Looker access token in a server-side module variable until it expires — re-authenticating on every API call wastes 200-400ms per request
- Never expose LOOKER_CLIENT_SECRET or LOOKER_EMBED_SECRET to the browser — all Looker API calls must happen in Next.js API routes
- Use a dedicated service account for Looker API access with only the permissions your app needs — not your personal admin account
- During development in Bolt's WebContainer, Looker API calls work as long as your Looker instance is publicly accessible — corporate VPN-protected Looker instances cannot be reached from the Bolt preview
- Map Looker's 'view_name.field_name' keys to human-readable labels in a config object — never display raw Looker field references to end users
- Set cache headers on your /api/looker routes for dashboard data that does not change frequently — Looker queries can be slow (1-5 seconds for complex models)
- Test SSO embed URLs with a short session_length (300 seconds) during development to quickly identify expiry issues without waiting an hour

## Use cases

### Embedded Analytics Dashboard

Embed a Looker dashboard directly inside your Bolt.new app using SSO embed URLs. Your users see a full interactive Looker dashboard — with filters, drilldowns, and visualizations — inside an iframe in your app, without being redirected to Looker's own UI. The embed URL is signed server-side with your Looker embed secret so users don't need Looker accounts.

Prompt example:

```
Build an embedded analytics page in my app that shows a Looker dashboard in an iframe. Create a Next.js API route at app/api/looker/embed-url/route.ts that generates a signed Looker SSO embed URL using the embed_secret from environment variables. The frontend page fetches this URL and renders it in an iframe that takes up the full content area. Add a loading skeleton while the URL is being fetched.
```

### Custom KPI Dashboard from Looker Data

Fetch data from a specific Looker Look (saved query) and display it in your own custom React charts and tables rather than Looker's iframe. This approach gives you complete control over the presentation while using Looker as the governed data source. Query results come back as JSON arrays.

Prompt example:

```
Build a KPI dashboard that fetches data from Looker. Create a Next.js API route app/api/looker/look/[lookId]/route.ts that authenticates with Looker using client_id and client_secret from environment variables, runs the Look with the given ID using the /looks/:look_id/run/json endpoint, and returns the results. Display the results as a responsive table with sorting, and as a bar chart using Recharts. Cache the Looker token in memory to avoid re-authenticating on every request.
```

### Inline Query Runner

Run an ad-hoc Looker inline query from your app — specify the model, view, dimensions, measures, and filters, and get back JSON results without needing a pre-saved Look. Useful for building dynamic report builders or allowing users to select their own dimensions and metrics.

Prompt example:

```
Build a query builder that sends inline queries to Looker. Create a Next.js API route app/api/looker/query/route.ts that accepts POST with a body containing model, view, fields (dimensions and measures), and filters. Authenticate with Looker, create a query using the /queries endpoint, run it with /queries/:query_id/run/json, and return the results. Build a simple UI where users select a model and fields from a dropdown, click 'Run Query', and see results in a table.
```

## Troubleshooting

### Looker login returns 401 Unauthorized — authentication fails

Cause: The client_id or client_secret is incorrect, the API key was deleted in the Looker Admin panel, or the user account associated with the key has been deactivated.

Solution: Verify the LOOKER_CLIENT_ID and LOOKER_CLIENT_SECRET values in your .env file match exactly what Looker showed when you created the key. Go to Looker Admin → Users → find the user → API Keys section to confirm the key exists. If the key was deleted, create a new one and update your .env file.

```
// Test your Looker credentials directly:
const resp = await fetch('https://yourcompany.looker.com/api/4.0/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: `client_id=${process.env.LOOKER_CLIENT_ID}&client_secret=${process.env.LOOKER_CLIENT_SECRET}`,
});
console.log(resp.status, await resp.json());
```

### SSO embed URL shows 'Invalid signature' in the Looker iframe

Cause: The signing algorithm has an error — typically wrong field order, incorrect embed_secret (not the same as the API client_secret), extra whitespace, or incorrect URL encoding.

Solution: Verify you are using LOOKER_EMBED_SECRET, not LOOKER_CLIENT_SECRET — these are different credentials found in different places in the Admin panel. Check Looker's documentation for the exact field order in the string-to-sign. Remove any trailing whitespace from each field. Use Looker's built-in embed URL validator in Admin → Platform → Embed if available on your instance.

### Looker API calls work locally in Bolt but fail after deploying to Netlify

Cause: The LOOKER_CLIENT_ID, LOOKER_CLIENT_SECRET, LOOKER_BASE_URL, and LOOKER_EMBED_SECRET environment variables were set in the .env file but not added to Netlify's environment variables.

Solution: In Netlify's dashboard, go to Site Configuration → Environment Variables and add all four Looker environment variables. The .env file is not deployed to Netlify — production environment variables must be set in the hosting dashboard. Trigger a redeploy after adding the variables.

### Look results return fields with 'null' values where data is expected

Cause: The Looker Look's query returns NULL for some rows (e.g., empty dimensions), or the user's API credentials do not have access to the underlying data model or database connection.

Solution: In the Looker UI, run the Look manually with the same API user's permissions to verify the data returns correctly. Check that the API key user has the 'access_data' permission on the relevant model. For null numeric values, use Number(row[field]) || 0 when mapping data to chart-friendly formats.

```
// Safe field extraction from Looker results
const normalized = rawData.map((row) => ({
  label: String(row[dimensionField] ?? 'Unknown'),
  value: Number(row[measureField]) || 0,
}));
```

## Frequently asked questions

### How do I authenticate with the Looker API from a Bolt.new app?

Use the OAuth 2.0 client credentials flow: POST your client_id and client_secret (as form-encoded body parameters) to /api/4.0/login on your Looker instance URL. The response includes an access_token valid for one hour. Include this token as 'Authorization: token {access_token}' in all subsequent API requests. Because the client_secret must be kept private, do all of this in a Next.js API route — never in browser-side code.

### Does Looker work with Bolt's WebContainer during development?

Yes, as long as your Looker instance is publicly accessible over the internet. Bolt's WebContainer can make outbound HTTP calls to Looker's API, and your API route code runs server-side in Next.js. The limitation is on incoming webhooks — Bolt's WebContainer cannot receive incoming connections. Looker's integration is entirely outbound (your app calls Looker), so the WebContainer limitation does not affect this integration.

### What permissions does the Looker API user need?

For running Looks and inline queries, the API user needs the 'access_data' and 'see_looks' permissions on the relevant models. For SSO embedding, the user generating embed URLs needs the 'embed_browse_spaces' permission or admin access. For managing users and groups via API, admin permissions are required. Create a dedicated service user with only the permissions your app needs.

### What is the difference between Looker's client_secret and embed_secret?

These are two completely separate credentials. The client_id and client_secret are API key credentials used to authenticate REST API requests — found in Admin → Users → API Keys. The embed_secret is a separate string used exclusively for signing SSO embed URLs — found in Admin → Platform → Embed. Using the client_secret to sign embed URLs will always result in 'Invalid signature' errors.

### Can I use Looker without a Google Cloud account?

Looker is available in two forms: Looker (Google Cloud core), which is a SaaS product on Google Cloud, and Looker-licensed (the original standalone product). Both require a paid license — there is no free tier or trial that provides API access. If your organization already has a Looker instance, ask your Looker administrator to create API credentials for you.

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/looker
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/looker
