# How to Integrate Bolt.new with LinkedIn Ads

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

## TL;DR

Connect Bolt.new to LinkedIn Marketing API by creating a LinkedIn developer app, requesting Marketing API product access, implementing the OAuth 2.0 authorization code flow via a deployed URL, and making API calls through a Next.js API route with your access token. Build dashboards showing campaign performance, spend, impressions, and B2B targeting analytics. LinkedIn's OAuth callback requires a deployed domain — it cannot use Bolt's WebContainer preview URL.

## Build a LinkedIn Ads Analytics Dashboard from Bolt.new

LinkedIn Ads is the dominant B2B advertising platform — the only place you can target by job title, company size, industry, and seniority simultaneously. For founders and marketers building B2B SaaS products, LinkedIn campaigns are often the highest-intent paid channel. The LinkedIn Marketing API lets you programmatically access your campaign data, build custom dashboards, automate reporting, and manage campaigns at scale — the kind of workflow that would otherwise require manually exporting data from Campaign Manager and pasting it into spreadsheets.

The LinkedIn Marketing API uses the same OAuth 2.0 authorization code flow as most Google and Meta APIs, but with one critical difference: LinkedIn's app review process is stricter. Getting Marketing API access requires creating a developer app, adding the 'Marketing Developer Platform' product, and waiting for LinkedIn to review and approve your application — a process that can take several days to weeks. The Marketing Developer Platform product access is required before any marketing-specific endpoints work. Without it, your app can authenticate users (Member login) but cannot access advertising data.

For Bolt.new developers, there is an important technical constraint: LinkedIn's OAuth callback URL must be a real HTTPS URL registered in your LinkedIn app settings. Bolt's WebContainer preview URL is ephemeral and cannot be registered as an OAuth redirect URI. This means you must deploy your app to Netlify or Vercel and register your deployed domain before you can complete the OAuth flow. This tutorial walks you through building the app in Bolt, deploying it, configuring the OAuth callback, and then connecting to the LinkedIn Marketing API for campaign analytics.

## Before you start

- A LinkedIn account with admin access to an active LinkedIn Ads Campaign Manager account
- A LinkedIn developer app created at developer.linkedin.com with the Marketing Developer Platform product requested and approved
- A Bolt.new project using Next.js (required for server-side OAuth handling and API routes)
- A deployed Netlify or Vercel URL to use as the OAuth redirect URI — LinkedIn cannot redirect to Bolt's WebContainer preview
- Your LinkedIn app's Client ID and Client Secret from the LinkedIn Developer portal

## Step-by-step guide

### 1. Create a LinkedIn Developer App and Request Marketing API Access

Go to developer.linkedin.com and click 'Create app'. You will need to associate the app with a LinkedIn Company Page — if you do not have one, create a company page first (it takes about 2 minutes at linkedin.com/company/setup). Fill in the app name, LinkedIn page, app logo, and agree to the terms. After creating the app, go to the 'Products' tab. You will see a list of LinkedIn API products. Find 'Marketing Developer Platform' and click 'Request access'. LinkedIn will review your application — this is the step where developers often wait. The review process can take from a few hours to several weeks depending on your use case description. While waiting, you can set up the rest of the app. Also request 'Sign In with LinkedIn using OpenID Connect' if you want user authentication. On the 'Auth' tab, add your redirect URLs. You must add both a development URL (you will use a deployed staging URL since Bolt preview cannot receive OAuth callbacks) and your production URL. LinkedIn requires HTTPS for all redirect URIs. Add your Netlify URL now — you will get it after your first deployment. LinkedIn's OAuth 2.0 scopes for Marketing API include: r_ads (read ad accounts), r_ads_reporting (read analytics), rw_ads (read/write for managing campaigns), and r_organization_social. Request only the scopes your app needs — unnecessary permissions slow down the review process.

```
// lib/linkedin.ts
const LINKEDIN_AUTH_URL = 'https://www.linkedin.com/oauth/v2/authorization';
const LINKEDIN_TOKEN_URL = 'https://www.linkedin.com/oauth/v2/accessToken';

export function buildAuthUrl(state: string): string {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: process.env.LINKEDIN_CLIENT_ID!,
    redirect_uri: process.env.LINKEDIN_REDIRECT_URI!,
    state,
    scope: 'r_ads r_ads_reporting',
  });
  return `${LINKEDIN_AUTH_URL}?${params.toString()}`;
}

export async function exchangeCodeForTokens(code: string) {
  const response = await fetch(LINKEDIN_TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: process.env.LINKEDIN_REDIRECT_URI!,
      client_id: process.env.LINKEDIN_CLIENT_ID!,
      client_secret: process.env.LINKEDIN_CLIENT_SECRET!,
    }),
  });
  if (!response.ok) throw new Error(`Token exchange failed: ${await response.text()}`);
  return response.json() as Promise<{
    access_token: string;
    expires_in: number;
    refresh_token: string;
    refresh_token_expires_in: number;
  }>;
}

export async function callLinkedInAPI(endpoint: string, accessToken: string) {
  const response = await fetch(`https://api.linkedin.com/v2${endpoint}`, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'LinkedIn-Version': '202401',
      'X-Restli-Protocol-Version': '2.0.0',
    },
  });
  if (!response.ok) throw new Error(`LinkedIn API error: ${response.status} ${await response.text()}`);
  return response.json();
}
```

**Expected result:** The LinkedIn app is created, Marketing Developer Platform access is requested, and the auth utility functions are in place. You have the Client ID and Client Secret from the LinkedIn Developer portal.

### 2. Deploy to Netlify First, Then Configure OAuth Redirect URI

This step is the most critical difference between LinkedIn Ads integration and simpler API integrations. LinkedIn's OAuth 2.0 authorization code flow requires a redirect URI — when the user authorizes your app on LinkedIn's website, LinkedIn redirects them back to your app at the URL you specify. This URL must be pre-registered in your LinkedIn developer app settings and must use HTTPS. Bolt's WebContainer preview URL is ephemeral (it changes between sessions) and is not publicly accessible from LinkedIn's servers — it cannot be used as an OAuth redirect URI. You must deploy your app to get a stable HTTPS URL before the OAuth flow can work. In Bolt, go to Settings → Applications and connect Netlify (or Vercel) via OAuth. Click Publish. After deployment, copy your Netlify URL (e.g., https://your-app-name.netlify.app). Go back to developer.linkedin.com → your app → Auth tab → Redirect URLs, and add https://your-app-name.netlify.app/api/auth/linkedin/callback. Update your LINKEDIN_REDIRECT_URI environment variable in Netlify's Site Configuration → Environment Variables and redeploy. For local development, you can use a tool like ngrok to create a temporary HTTPS tunnel to your local server and register that as a development redirect URI — but this is optional. Note: Bolt's WebContainer during development cannot receive incoming OAuth callbacks. This is a fundamental WebContainer limitation — the browser-based runtime has no public URL. Build and test the non-OAuth parts of your app in the preview, and test the full OAuth flow on your deployed Netlify URL.

```
// app/api/auth/linkedin/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { exchangeCodeForTokens } from '@/lib/linkedin';
import { cookies } from 'next/headers';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');
  const state = searchParams.get('state');
  const error = searchParams.get('error');

  if (error) {
    return NextResponse.redirect(new URL('/login?error=linkedin_denied', request.url));
  }

  // Validate CSRF state
  const cookieStore = await cookies();
  const savedState = cookieStore.get('linkedin_oauth_state')?.value;
  if (!state || state !== savedState) {
    return NextResponse.redirect(new URL('/login?error=invalid_state', request.url));
  }

  if (!code) {
    return NextResponse.redirect(new URL('/login?error=no_code', request.url));
  }

  const tokens = await exchangeCodeForTokens(code);

  const response = NextResponse.redirect(new URL('/dashboard', request.url));
  // Store tokens in HTTP-only cookies (production: encrypt with NEXTAUTH_SECRET)
  response.cookies.set('linkedin_access_token', tokens.access_token, {
    httpOnly: true,
    secure: true,
    maxAge: tokens.expires_in,
    path: '/',
  });
  response.cookies.set('linkedin_refresh_token', tokens.refresh_token, {
    httpOnly: true,
    secure: true,
    maxAge: tokens.refresh_token_expires_in,
    path: '/',
  });
  // Clear the state cookie
  response.cookies.delete('linkedin_oauth_state');

  return response;
}
```

**Expected result:** After deploying to Netlify and registering the callback URL, users can complete the LinkedIn OAuth flow. The access_token is stored in an HTTP-only cookie after successful authorization.

### 3. Fetch Campaign Performance Data from Marketing API

With an access token obtained from the OAuth flow, you can now query LinkedIn's Marketing API for campaign analytics. The key endpoints are: /adAccountsV2 (list ad accounts the user has access to), /adCampaignsV2 (list campaigns within an account), and /adAnalyticsV2 (fetch performance metrics). The analytics endpoint is the most powerful — it supports filtering by campaign, date range, and demographic dimensions, and returns metrics like impressions, clicks, spend (in micro-currency units — divide by 1,000,000 for USD), conversions, and conversion value. LinkedIn's API uses the 'finder' pattern for analytics: you POST a query specifying pivots (dateRange, campaign), timeGranularity (DAILY, MONTHLY, ALL), and metrics (impressions, clicks, externalWebsiteConversions, costInLocalCurrency). The response is a collection of data points. LinkedIn's spend values are returned in micro-currency (e.g., 5000000 means $5.00 USD) — always divide by 1,000,000 when displaying. Similarly, LinkedIn uses URN-formatted identifiers (e.g., 'urn:li:sponsoredCampaign:12345678') — extract the numeric ID for display. All Marketing API calls must include the 'LinkedIn-Version' header set to the API version date (e.g., '202401') and the 'X-Restli-Protocol-Version: 2.0.0' header. During development in Bolt's WebContainer, you cannot test this end-to-end because the OAuth flow requires your deployed URL. You can, however, build and style the dashboard UI components in the Bolt preview using mock data, then connect the real API after deployment.

```
// app/api/linkedin/analytics/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';

export async function GET(request: NextRequest) {
  const cookieStore = await cookies();
  const accessToken = cookieStore.get('linkedin_access_token')?.value;

  if (!accessToken) {
    return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
  }

  const { searchParams } = new URL(request.url);
  const accountId = searchParams.get('accountId');
  const daysBack = parseInt(searchParams.get('days') || '30');

  const endDate = new Date();
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - daysBack);

  const params = new URLSearchParams({
    q: 'analytics',
    pivot: 'CAMPAIGN',
    dateRange: JSON.stringify({
      start: { day: startDate.getDate(), month: startDate.getMonth() + 1, year: startDate.getFullYear() },
      end: { day: endDate.getDate(), month: endDate.getMonth() + 1, year: endDate.getFullYear() },
    }),
    timeGranularity: 'MONTHLY',
    accounts: `List(urn:li:sponsoredAccount:${accountId})`,
    fields: 'impressions,clicks,externalWebsiteConversions,costInLocalCurrency,dateRange',
  });

  const response = await fetch(`https://api.linkedin.com/v2/adAnalyticsV2?${params}`, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'LinkedIn-Version': '202401',
      'X-Restli-Protocol-Version': '2.0.0',
    },
  });

  if (!response.ok) {
    return NextResponse.json({ error: await response.text() }, { status: response.status });
  }

  const raw = await response.json();
  // LinkedIn spend is in micro-currency — divide by 1,000,000 for USD
  const data = (raw.elements || []).map((el: Record<string, unknown>) => ({
    ...el,
    spendUSD: Number(el.costInLocalCurrency) / 1_000_000,
    ctr: el.impressions ? ((Number(el.clicks) / Number(el.impressions)) * 100).toFixed(2) : '0.00',
  }));

  return NextResponse.json({ data });
}
```

**Expected result:** The analytics API route returns campaign metrics including impressions, clicks, spend in USD, and conversions. The dashboard page displays KPI cards and a trend chart with real LinkedIn Ads data.

## Best practices

- Deploy to Netlify or Vercel before testing the LinkedIn OAuth flow — Bolt's WebContainer preview cannot receive OAuth redirect callbacks from LinkedIn
- Store access tokens in HTTP-only cookies, not localStorage — HTTP-only cookies are inaccessible to JavaScript and protected from XSS attacks
- Request only the OAuth scopes you need (r_ads for account data, r_ads_reporting for analytics) — unnecessary scope requests slow down LinkedIn's app review process
- Always divide LinkedIn spend values by 1,000,000 — costInLocalCurrency and all budget fields use micro-currency units
- Implement token refresh logic using the refresh_token before the access_token expires (LinkedIn access tokens last 60 days, refresh tokens last 365 days)
- Add a CSRF state parameter to the OAuth authorization URL and validate it in the callback to prevent state forgery attacks
- Cache campaign list data with a short TTL (5-15 minutes) — LinkedIn's Marketing API has rate limits and campaign metadata does not change frequently

## Use cases

### LinkedIn Ads Performance Dashboard

Build a real-time dashboard showing your LinkedIn campaign performance — impressions, clicks, click-through rate, conversions, cost per conversion, and total spend — broken down by campaign or ad. Fetch this data from the LinkedIn Marketing API and display it in charts and tables tailored for B2B metrics reporting.

Prompt example:

```
Build a LinkedIn Ads analytics dashboard. Create a Next.js API route at app/api/linkedin/campaigns/route.ts that fetches campaign analytics from the LinkedIn Marketing API using an OAuth access token stored in session. Display metrics in cards at the top: total impressions, total clicks, total spend (formatted as currency), and average CTR. Below, show a table of campaigns with columns for campaign name, status, impressions, clicks, CTR, conversions, and spend. Add a date range filter (last 7 days, 30 days, 90 days).
```

### B2B Audience Targeting Analyzer

Analyze which audience segments are performing best in your LinkedIn campaigns — which job titles, companies, industries, or seniority levels drive the most conversions at the lowest cost. Fetch demographic breakdown data from the LinkedIn Marketing API and visualize it as ranked charts.

Prompt example:

```
Build a LinkedIn audience analyzer page. Create an API route that fetches demographic breakdown analytics from the LinkedIn Marketing API for a given campaign. Show a bar chart of performance by job function (with impressions and conversions per segment), a ranked list of top-performing company industries, and a table comparing seniority levels by cost-per-conversion. Use Recharts for the visualizations. This helps identify which B2B audience segments are most valuable.
```

### Multi-Account Ad Spend Tracker

For agencies or businesses managing multiple LinkedIn Ads accounts, build a consolidated spend tracker that shows total daily and monthly spend across all linked accounts, budget utilization percentage, and spend pacing. Pull data from the Marketing API for each account and aggregate it in a single view.

Prompt example:

```
Build a LinkedIn Ads spend tracker for multiple ad accounts. After LinkedIn OAuth authentication, fetch all ad accounts the user has access to via the /adAccountsV2 endpoint. For each account, fetch the current month's total spend using the analytics endpoint. Display a summary card per account showing: account name, account ID, monthly budget, current month spend, and spend pacing (percentage of monthly budget used as of today). Show a total across all accounts at the top.
```

## Troubleshooting

### OAuth redirect fails with 'redirect_uri_mismatch' error on LinkedIn's authorization page

Cause: The redirect_uri in your authorization URL does not exactly match the URL registered in your LinkedIn developer app's Auth settings. LinkedIn requires an exact string match including trailing slashes.

Solution: In developer.linkedin.com → your app → Auth tab → Redirect URLs, verify the registered URL matches exactly what your LINKEDIN_REDIRECT_URI environment variable contains. Even a trailing slash difference causes this error. Also ensure you are using your deployed Netlify URL — Bolt's WebContainer preview URL cannot be registered.

```
// Ensure the redirect_uri in buildAuthUrl() exactly matches what LinkedIn has registered
// Example: https://your-app.netlify.app/api/auth/linkedin/callback
// NOT: https://your-app.netlify.app/api/auth/linkedin/callback/  (trailing slash differs)
```

### LinkedIn Marketing API returns 403 Forbidden with 'Access denied' when calling /adAccountsV2

Cause: The Marketing Developer Platform product has not been approved yet, or the OAuth scopes requested (r_ads, r_ads_reporting) were not included when the user authorized the app.

Solution: Check your LinkedIn developer app's Products tab — Marketing Developer Platform must show 'Added' status, not 'Pending review'. If it is still pending, wait for approval. If already approved, ensure the OAuth authorization URL includes scope=r_ads r_ads_reporting as URL-encoded scope parameters. Have the user re-authorize to get a new token with the correct scopes.

### LinkedIn spend values appear as large numbers like 5000000 instead of $5.00

Cause: LinkedIn Marketing API returns cost values in micro-currency units — 1,000,000 units equals 1 unit of the account currency (e.g., $1.00 USD).

Solution: Divide all costInLocalCurrency, totalBudget, and spend-related fields by 1,000,000 when displaying to users.

```
// Convert micro-currency to currency
const spendUSD = Number(element.costInLocalCurrency) / 1_000_000;
const formatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(spendUSD);
```

### LinkedIn OAuth flow works on deployed Netlify but returns 'invalid_redirect_uri' during local development in Bolt's preview

Cause: Bolt's WebContainer preview URL is not a stable HTTPS URL and cannot be registered as an OAuth redirect URI. LinkedIn requires a pre-registered HTTPS URL for all redirects.

Solution: This is a known WebContainer limitation — incoming OAuth redirects cannot be received during development. Test the LinkedIn OAuth flow using your deployed Netlify URL. For the dashboard UI components, use mock data during development in Bolt's preview, then connect the real API after deployment.

## Frequently asked questions

### Can I test LinkedIn OAuth in Bolt's WebContainer preview?

No. LinkedIn's OAuth flow redirects the user to LinkedIn's website and then back to your app's callback URL. Bolt's WebContainer during development does not have a public URL that LinkedIn can redirect to. You must deploy your app to Netlify or Vercel, register your deployed URL in the LinkedIn developer portal, and test the OAuth flow on the deployed app.

### How long does it take to get LinkedIn Marketing API access approved?

LinkedIn's review of the Marketing Developer Platform product access takes anywhere from a few hours to several weeks. The review is faster if you provide a detailed description of your use case and already have a deployed application with the app logo and LinkedIn company page properly set up. Personal developer accounts without company associations tend to face longer review times.

### What scopes do I need for reading LinkedIn Ads campaign performance data?

To read campaign performance analytics, you need r_ads (read access to ad accounts and campaign structures) and r_ads_reporting (read access to campaign analytics and reporting data). If you also need to create or modify campaigns, add rw_ads. Request only the scopes you need — broader scope requests can slow down LinkedIn's review process.

### Why are LinkedIn spend values showing as very large numbers?

LinkedIn's Marketing API returns cost values in micro-currency — 1,000,000 units equals 1 currency unit (e.g., $1.00 USD or €1.00 EUR). Always divide costInLocalCurrency and all budget-related fields by 1,000,000 before displaying them. This is a common gotcha for developers new to LinkedIn's Marketing API.

### How do I handle LinkedIn token expiry in my Bolt.new app?

LinkedIn access tokens expire after 60 days, and refresh tokens expire after 365 days. Implement a token refresh flow: before making Marketing API calls, check the cookie's expiry or make the call and catch 401 errors. If expired, use the refresh_token to call /oauth/v2/accessToken with grant_type=refresh_token to get a new access_token without requiring the user to re-authorize.

---

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