Keap (formerly Infusionsoft) integrates with Bolt.new through its REST API v2 using OAuth 2.0 for authorization. Create a Keap developer app, implement the OAuth flow via a Next.js API route, and manage contacts, deals, and automated email campaigns. Phone features require Twilio since Keap's built-in business phone is not exposed in the API. OAuth callbacks require a deployed URL.
Building a Keap CRM Integration in Bolt.new
Keap is the simplified, modern evolution of Infusionsoft — the same platform rebranded to appeal to small businesses that want CRM and marketing automation without enterprise complexity. With 200,000+ small business users, Keap combines contact management, deal pipelines, email marketing, invoicing, and appointment booking. For Bolt.new developers, integrating Keap enables building custom business dashboards, contact sync tools, and lead management interfaces that connect directly to their clients' Keap data.
Keap's REST API v2 is a well-structured HTTP API that uses OAuth 2.0 for authorization. Unlike API-key-only services, Keap requires users to authorize your application through an OAuth flow — meaning your app gets access to the specific Keap account the user authorizes, making it suitable for multi-tenant tools (an agency managing multiple clients' Keap accounts) or a custom dashboard for a single business's own data. The OAuth flow requires a publicly accessible callback URL, which means you must deploy your Bolt app before you can complete the authorization handshake for the first time.
One important clarification about phone features: Keap includes a built-in business phone number in some plans, and the 'keap phone integration' query commonly brings people to this page. However, Keap's API does not expose call logs, SMS sending, or phone number management. For phone and SMS features alongside your Keap CRM data, you need to integrate Twilio separately. This guide covers the core CRM API capabilities — contacts, deals, and emails — and explains how to pair Twilio for phone features. The modern Keap product uses REST API v2; if you are using the legacy Infusionsoft/Keap Classic product, refer to the separate Infusionsoft page which covers the XML-RPC and legacy REST endpoints.
Integration method
Keap integrates with Bolt.new through its REST API v2 using OAuth 2.0. You register a developer app on developer.keap.com, implement the OAuth authorization code flow through Next.js API routes (which requires a deployed callback URL), and store the access and refresh tokens securely. All subsequent API calls — reading contacts, creating deals, sending emails — go through server-side API routes using the stored tokens. Keap's built-in business phone feature is not available through the API; use Twilio for phone and SMS capabilities alongside Keap.
Prerequisites
- A Keap account (keap.com) — the Pro plan ($159/month) includes the REST API; confirm your plan has API access in your account settings
- A Keap developer application registered at developer.keap.com — you need the Client ID and Client Secret from the developer portal
- A deployed Bolt.new app URL on Netlify or Bolt Cloud for the OAuth callback redirect — the OAuth authorization flow cannot complete in Bolt's WebContainer preview without a stable redirect URI
- A Next.js project in Bolt.new for the API route pattern — Keap's OAuth flow and API calls must run server-side to keep credentials secure
- Familiarity with OAuth 2.0 authorization code flow — the authorization, code exchange, and token refresh cycle is required for Keap API access
Step-by-step guide
Register a Keap developer application and configure OAuth credentials
Register a Keap developer application and configure OAuth credentials
Before writing any code, you need to register your application in Keap's developer portal and obtain the OAuth credentials that your Bolt.new app will use to request authorization. Go to developer.keap.com and sign in with your Keap account. Navigate to 'My Apps' and click 'Create New App.' Give your application a name that describes its purpose (e.g., 'My Business Dashboard'), enter a description, and upload a logo if you have one. The most important setting is the Redirect URI (also called Callback URL). This is the URL Keap will send users back to after they authorize your app — it must be your deployed application URL, not the Bolt WebContainer preview URL. Enter https://yourapp.netlify.app/api/auth/keap/callback (using your actual deployed domain). You can add multiple redirect URIs — add one for each environment you plan to use (staging and production). The Bolt preview URL cannot be registered as a redirect URI because it changes with each session. After saving, Keap generates a Client ID and Client Secret. Copy both values immediately and store them securely. Add KEAP_CLIENT_ID and KEAP_CLIENT_SECRET to your Bolt project's .env file. Never commit these values to Git or expose them in client-side code. Keap's OAuth scopes determine what data your app can access. The relevant scopes for a CRM dashboard are: full (complete access to all CRM data), or more granular options like contacts (read/write contacts), opportunities (deals), and marketing (email campaigns). For a general-purpose integration, request the full scope and let users see the full permission request during authorization. For a specific-purpose tool (e.g., read-only contact sync), request only the scopes you need. Note on Keap plans: the REST API v2 is available on Keap Pro ($159/month) and Max ($229/month). The Keap Lite plan ($79/month) has limited API access. Verify your subscription level includes the API capabilities you need before building your integration.
Set up Keap OAuth configuration in this Next.js app. Add KEAP_CLIENT_ID, KEAP_CLIENT_SECRET, and KEAP_REDIRECT_URI to .env with placeholder values. Create a utility file at lib/keap/config.ts that exports the Keap OAuth authorization URL builder function: it should construct the URL https://accounts.infusionsoft.com/app/oauth/authorize with params client_id, redirect_uri, response_type=code, scope=full, and a random state parameter for CSRF protection. Export the token endpoint constant (https://api.infusionsoft.com/token) and base API URL (https://api.infusionsoft.com/crm/rest/v2). Add TypeScript interfaces for the OAuth token response and the stored token shape.
Paste this in Bolt.new chat
1// lib/keap/config.ts2export const KEAP_OAUTH_BASE = 'https://accounts.infusionsoft.com/app/oauth/authorize';3export const KEAP_TOKEN_URL = 'https://api.infusionsoft.com/token';4export const KEAP_API_BASE = 'https://api.infusionsoft.com/crm/rest/v2';56export interface KeapTokenResponse {7 access_token: string;8 refresh_token: string;9 token_type: string;10 expires_in: number;11 scope: string;12}1314export interface StoredKeapToken {15 accessToken: string;16 refreshToken: string;17 expiresAt: number; // Unix timestamp in milliseconds18 scope: string;19}2021export function buildAuthorizationUrl(state: string): string {22 const params = new URLSearchParams({23 client_id: process.env.KEAP_CLIENT_ID ?? '',24 redirect_uri: process.env.KEAP_REDIRECT_URI ?? '',25 response_type: 'code',26 scope: 'full',27 state,28 });29 return `${KEAP_OAUTH_BASE}?${params.toString()}`;30}3132export function isTokenExpired(token: StoredKeapToken): boolean {33 // Consider expired 5 minutes before actual expiry34 return Date.now() >= token.expiresAt - 5 * 60 * 1000;35}Pro tip: Keap's authorization URL uses accounts.infusionsoft.com (not keap.com) — this is the legacy domain that Keap retained for OAuth even after the rebrand. Both Keap and Infusionsoft accounts use the same authorization server.
Expected result: The Keap OAuth configuration is in place. The .env file has placeholder values for Client ID, Client Secret, and Redirect URI. The config module exports the authorization URL builder and token interfaces for use in the API routes.
Implement the OAuth 2.0 authorization code flow in Next.js API routes
Implement the OAuth 2.0 authorization code flow in Next.js API routes
The OAuth flow has three phases: (1) redirect the user to Keap's authorization page, (2) receive the authorization code at your callback URL, and (3) exchange the code for access and refresh tokens. All three phases are implemented as Next.js API routes. The first route, GET /api/auth/keap, generates a random state parameter for CSRF protection (store it in a cookie or session), builds the Keap authorization URL using your Client ID and Redirect URI, and redirects the user to Keap's login page. The user sees a Keap login prompt asking them to authorize your app. After authorizing, Keap redirects them back to your KEAP_REDIRECT_URI with a code and state query parameter. The second route, GET /api/auth/keap/callback, receives the redirect from Keap. It validates the state parameter matches what you set (CSRF protection), extracts the authorization code from query params, and makes a server-side POST request to Keap's token endpoint to exchange the code for tokens. The token exchange requires your Client ID and Client Secret — this is why the exchange must happen server-side. The response contains an access_token (valid for ~24 hours), a refresh_token (long-lived), and expires_in (seconds until expiry). Store the tokens securely. For a single-user app (the business owner's own dashboard), storing tokens in environment variables works. For a multi-user app (serving multiple Keap accounts), store tokens in your database encrypted, associated with the user's account. The access token expires and must be refreshed using the refresh token before it expires. Token refresh is handled by a utility function that checks if the access token is within 5 minutes of expiry. If so, it makes a POST to the token endpoint with grant_type=refresh_token and the stored refresh token. The token endpoint returns new access and refresh tokens — always save both, as Keap rotates refresh tokens on each use. Critical: this entire OAuth flow requires a deployed application URL. The authorization callback (https://yourapp.netlify.app/api/auth/keap/callback) must be publicly accessible on the internet so Keap can redirect users back to it. You cannot test the full OAuth flow in Bolt's WebContainer because the preview URL is not a stable publicly accessible HTTPS URL that Keap can register as a redirect URI.
Implement Keap OAuth flow with these Next.js API routes. (1) app/api/auth/keap/route.ts: generate a random state string, save it in a cookie called 'keap_oauth_state', and redirect to the Keap authorization URL using buildAuthorizationUrl from lib/keap/config.ts. (2) app/api/auth/keap/callback/route.ts: validate the state cookie matches, extract the 'code' param, exchange it for tokens by POSTing to KEAP_TOKEN_URL with grant_type=authorization_code, code, client_id, client_secret, and redirect_uri, then save the access_token and refresh_token to .env placeholder or log them for now, and redirect to /dashboard. Handle errors with a redirect to /auth/error. Add TypeScript types throughout.
Paste this in Bolt.new chat
1// app/api/auth/keap/callback/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { KEAP_TOKEN_URL, type KeapTokenResponse } from '@/lib/keap/config';45export async function GET(request: NextRequest) {6 const { searchParams } = new URL(request.url);7 const code = searchParams.get('code');8 const state = searchParams.get('state');9 const stateCookie = request.cookies.get('keap_oauth_state')?.value;1011 // Validate CSRF state12 if (!state || state !== stateCookie) {13 return NextResponse.redirect(new URL('/auth/error?reason=invalid_state', request.url));14 }15 if (!code) {16 return NextResponse.redirect(new URL('/auth/error?reason=no_code', request.url));17 }1819 try {20 // Exchange authorization code for tokens21 const credentials = Buffer.from(22 `${process.env.KEAP_CLIENT_ID}:${process.env.KEAP_CLIENT_SECRET}`23 ).toString('base64');2425 const tokenResponse = await fetch(KEAP_TOKEN_URL, {26 method: 'POST',27 headers: {28 'Authorization': `Basic ${credentials}`,29 'Content-Type': 'application/x-www-form-urlencoded',30 },31 body: new URLSearchParams({32 grant_type: 'authorization_code',33 code,34 redirect_uri: process.env.KEAP_REDIRECT_URI ?? '',35 }),36 });3738 if (!tokenResponse.ok) {39 const error = await tokenResponse.text();40 console.error('Keap token exchange failed:', error);41 return NextResponse.redirect(new URL('/auth/error?reason=token_exchange', request.url));42 }4344 const tokens: KeapTokenResponse = await tokenResponse.json();4546 // In production: save tokens to your database associated with the user session47 // For now, log for development reference:48 console.log('Keap OAuth successful. Access token received. Expires in:', tokens.expires_in, 's');4950 // Set access token in a secure HTTP-only cookie (simple single-user approach)51 const response = NextResponse.redirect(new URL('/dashboard', request.url));52 response.cookies.set('keap_access_token', tokens.access_token, {53 httpOnly: true,54 secure: process.env.NODE_ENV === 'production',55 maxAge: tokens.expires_in,56 sameSite: 'lax',57 });58 response.cookies.set('keap_refresh_token', tokens.refresh_token, {59 httpOnly: true,60 secure: process.env.NODE_ENV === 'production',61 maxAge: 60 * 60 * 24 * 30, // 30 days62 sameSite: 'lax',63 });64 response.cookies.delete('keap_oauth_state');6566 return response;67 } catch (error) {68 console.error('OAuth callback error:', error);69 return NextResponse.redirect(new URL('/auth/error?reason=server_error', request.url));70 }71}Pro tip: For production multi-user apps, store Keap tokens in your database (encrypted) rather than in cookies. Cookies work for single-user tools but are not suitable when your app serves multiple clients with different Keap accounts. Use a tokens table in Supabase with user_id, access_token (encrypted), refresh_token (encrypted), and expires_at.
Expected result: After deploying and registering the callback URL in the Keap developer portal, clicking 'Connect Keap' redirects to Keap's login page. After authorizing, the user is redirected back to /dashboard with auth tokens stored in HTTP-only cookies.
Fetch and display Keap contacts in a CRM dashboard
Fetch and display Keap contacts in a CRM dashboard
With OAuth tokens secured, you can now make authenticated calls to Keap's REST API v2 to fetch contact data. Keap's contacts endpoint returns a paginated list of contacts with fields including given_name, family_name, email_addresses (array), phone_numbers (array), company_name, date_created, last_updated, tags, and custom fields. The contacts list API call is GET https://api.infusionsoft.com/crm/rest/v2/contacts. Key query parameters include: limit (max 200, default 25), offset (for pagination), order (field to sort by, e.g., id, date_created), order_direction (ascending or descending), email (filter by email address), given_name (filter by first name), and family_name (filter by last name). The response includes a contact_list array and a count for total results. Create a utility function that makes authenticated Keap API calls. This function reads the access token from the HTTP-only cookie, adds the Authorization: Bearer {token} header, handles token refresh if needed, and returns the JSON response. All Keap API calls use this same utility — it is the single place where token reading and refresh logic lives. For the contacts dashboard component, use a combination of server-side initial data load and client-side search. The page initially loads the 25 most recent contacts from the server. A search input calls a client-side fetch to the API route with a search term, which calls Keap's contacts endpoint with the appropriate filter parameters. This pattern keeps initial page load fast and makes search feel responsive. Keap contacts have a complex email_addresses structure — it is an array of objects with email_address and field (EMAIL1, EMAIL2) properties. Extract the primary email with email_addresses.find(e => e.field === 'EMAIL1')?.email_address. Similarly, phone_numbers is an array with number and field (PHONE1, PHONE2, etc.) properties. The dashboard should handle contacts that have no email or phone gracefully — Keap does not require these fields.
Build a Keap contacts dashboard. Create a Next.js API route at app/api/keap/contacts/route.ts that reads the Keap access token from the 'keap_access_token' cookie, calls GET https://api.infusionsoft.com/crm/rest/v2/contacts?limit=25&order=date_created&order_direction=descending with the Bearer token, and returns the contact list. Build a contacts page at app/contacts/page.tsx that fetches from this route and displays contacts in a table with columns: Name, Email, Company, Phone, and Date Added. Add a search input that filters by name or email. Add a 'Connect Keap' button that links to /api/auth/keap if there is no active token. Use Tailwind CSS and shadcn/ui Table.
Paste this in Bolt.new chat
1// app/api/keap/contacts/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { KEAP_API_BASE } from '@/lib/keap/config';45interface KeapContact {6 id: number;7 given_name: string;8 family_name: string;9 company_name?: string;10 email_addresses: Array<{ email_address: string; field: string }>;11 phone_numbers: Array<{ number: string; field: string }>;12 date_created: string;13 last_updated: string;14}1516interface KeapContactsResponse {17 contacts: KeapContact[];18 count: number;19}2021export async function GET(request: NextRequest) {22 const token = request.cookies.get('keap_access_token')?.value;2324 if (!token) {25 return NextResponse.json({ error: 'Not authenticated with Keap' }, { status: 401 });26 }2728 const { searchParams } = new URL(request.url);29 const searchEmail = searchParams.get('email');30 const searchName = searchParams.get('given_name');31 const limit = searchParams.get('limit') ?? '25';32 const offset = searchParams.get('offset') ?? '0';3334 const params = new URLSearchParams({35 limit,36 offset,37 order: 'date_created',38 order_direction: 'descending',39 });40 if (searchEmail) params.set('email', searchEmail);41 if (searchName) params.set('given_name', searchName);4243 try {44 const response = await fetch(`${KEAP_API_BASE}/contacts?${params}`, {45 headers: {46 Authorization: `Bearer ${token}`,47 'Content-Type': 'application/json',48 },49 });5051 if (response.status === 401) {52 return NextResponse.json({ error: 'Keap token expired — re-authorize' }, { status: 401 });53 }5455 if (!response.ok) {56 return NextResponse.json(57 { error: `Keap API error: ${response.status}` },58 { status: response.status }59 );60 }6162 const data: KeapContactsResponse = await response.json();6364 const contacts = data.contacts.map((c) => ({65 id: c.id,66 name: [c.given_name, c.family_name].filter(Boolean).join(' ') || 'Unnamed',67 email: c.email_addresses.find((e) => e.field === 'EMAIL1')?.email_address ?? '',68 phone: c.phone_numbers.find((p) => p.field === 'PHONE1')?.number ?? '',69 company: c.company_name ?? '',70 dateAdded: new Date(c.date_created).toLocaleDateString(),71 }));7273 return NextResponse.json({ contacts, total: data.count });74 } catch (error) {75 const message = error instanceof Error ? error.message : 'Request failed';76 return NextResponse.json({ error: message }, { status: 500 });77 }78}Pro tip: Keap's API limits contacts responses to a maximum of 200 records per call. For businesses with large contact lists (thousands of records), implement pagination in your dashboard — add offset-based pagination controls that increment by your limit value and fetch the next page.
Expected result: The contacts dashboard shows a paginated table of Keap contacts with name, email, company, and phone columns. The search input filters results by calling the API route with filter parameters. An unauthenticated user sees a 'Connect Keap' button.
Create Keap contacts and apply tags via a lead capture form
Create Keap contacts and apply tags via a lead capture form
A common Keap integration pattern is creating contacts from form submissions — a lead captures their information on your Bolt-built landing page, and that contact automatically appears in Keap tagged and ready for follow-up. The Keap REST API v2 POST /contacts endpoint creates new contacts. Applying tags after creation requires a separate POST to /contacts/{id}/tags. The create contact request body uses a specific JSON structure. Email addresses and phone numbers are arrays of objects rather than simple strings. The email_addresses array item format is { email_address: 'user@example.com', field: 'EMAIL1' }. The phone_numbers format is { number: '+15551234567', field: 'PHONE1', type: 'Work' }. All field names for additional email/phone slots follow the pattern EMAIL2, EMAIL3, PHONE2, etc. Tag application in Keap requires knowing the tag ID, not the tag name. Get tag IDs by fetching GET /tags in your Keap account, or look them up in your Keap admin panel under Marketing → Tags. Store frequently used tag IDs as constants in your app (e.g., const WEBSITE_LEAD_TAG_ID = 456). The tag application endpoint is POST /contacts/{contactId}/tags with body { tagIds: [456] }. For duplicate contact handling: Keap returns a 422 status with an error body when you try to create a contact with an email that already exists. The error response includes the existing contact's ID in the error details. You can use this to update the existing contact instead of creating a new one — a common pattern called 'upsert'. Alternatively, search for the contact first with GET /contacts?email={email} and only create if no result is found. After creating the contact, you can also add them to a Keap email sequence (called a Campaign) using the POST /contacts/{id}/sequences endpoint. Sequences require the sequence ID, which you get from your Keap Campaign Builder. Sending contacts into sequences programmatically enables fully automated email nurture flows triggered by form submissions on your Bolt-built website.
Build a lead capture form with Keap integration. Create a server action in app/actions/keap.ts that accepts formData with fields: firstName, lastName, email, phone, company. It should: (1) POST to https://api.infusionsoft.com/crm/rest/v2/contacts with the Keap access token cookie, body including given_name, family_name, email_addresses array, and phone_numbers array, (2) If 201 success, apply tag ID from process.env.KEAP_LEAD_TAG_ID to the new contact via POST /contacts/{id}/tags, (3) Return success or error. Build a landing page with the form using react-hook-form and zod validation. Show a success message after submission. Handle the 422 duplicate contact error gracefully with a 'You're already in our system' message.
Paste this in Bolt.new chat
1// app/actions/keap.ts2'use server';3import { cookies } from 'next/headers';4import { KEAP_API_BASE } from '@/lib/keap/config';56interface CreateContactInput {7 firstName: string;8 lastName: string;9 email: string;10 phone?: string;11 company?: string;12}1314export async function createKeapContact(input: CreateContactInput) {15 const cookieStore = await cookies();16 const token = cookieStore.get('keap_access_token')?.value;1718 // For server-to-server integrations, use a service account token19 const accessToken = token ?? process.env.KEAP_SERVICE_ACCOUNT_TOKEN;2021 if (!accessToken) {22 return { error: 'Keap not configured' };23 }2425 const contactBody: Record<string, unknown> = {26 given_name: input.firstName,27 family_name: input.lastName,28 email_addresses: [{ email_address: input.email, field: 'EMAIL1' }],29 };3031 if (input.phone) {32 contactBody.phone_numbers = [{ number: input.phone, field: 'PHONE1', type: 'Work' }];33 }34 if (input.company) {35 contactBody.company_name = input.company;36 }3738 try {39 const createResponse = await fetch(`${KEAP_API_BASE}/contacts`, {40 method: 'POST',41 headers: {42 Authorization: `Bearer ${accessToken}`,43 'Content-Type': 'application/json',44 },45 body: JSON.stringify(contactBody),46 });4748 if (createResponse.status === 422) {49 // Contact with this email already exists50 return { success: true, alreadyExists: true };51 }5253 if (!createResponse.ok) {54 return { error: `Failed to create contact: ${createResponse.status}` };55 }5657 const newContact = await createResponse.json();58 const contactId: number = newContact.id;5960 // Apply lead tag if configured61 const tagId = process.env.KEAP_LEAD_TAG_ID;62 if (tagId && contactId) {63 await fetch(`${KEAP_API_BASE}/contacts/${contactId}/tags`, {64 method: 'POST',65 headers: {66 Authorization: `Bearer ${accessToken}`,67 'Content-Type': 'application/json',68 },69 body: JSON.stringify({ tagIds: [parseInt(tagId)] }),70 });71 }7273 return { success: true, contactId };74 } catch (error) {75 const message = error instanceof Error ? error.message : 'Request failed';76 return { error: message };77 }78}Pro tip: For landing page lead capture (where you want to create contacts without a user OAuth session), generate a Keap Service Account Key instead of relying on user OAuth tokens. Go to your Keap admin → Settings → API to generate a service account key that gives programmatic access to your own Keap account without OAuth. Store it as KEAP_SERVICE_ACCOUNT_TOKEN.
Expected result: Submitting the lead capture form creates a new contact in Keap, applies the specified lead tag, and shows a success message. The new contact appears in the Keap admin panel under Contacts within a few seconds. Duplicate email submissions show the 'already in our system' message.
Common use cases
Custom Small Business CRM Dashboard
A React dashboard that displays a company's Keap contacts, deals, and recent activity in a custom interface. The business owner connects their Keap account via OAuth, and the dashboard shows open deals, recent contacts, and upcoming follow-up tasks. All data fetches go through Next.js API routes using the stored OAuth token.
Build a Keap CRM dashboard. Create Next.js API routes: (1) /api/keap/contacts that calls GET https://api.infusionsoft.com/crm/rest/v2/contacts with the Keap OAuth token from process.env.KEAP_ACCESS_TOKEN and returns contacts with id, email_addresses, given_name, family_name, company_name, and phone_numbers. (2) /api/keap/opportunities that fetches open deals. Build a dashboard page with two sections: a contacts list with search, and an open deals pipeline. Use Tailwind CSS and shadcn/ui Table component for the contacts list.
Copy this prompt to try it in Bolt.new
Lead Capture Form with Keap Contact Creation
A landing page with a lead capture form that creates a new contact in Keap when submitted, tags them with a specific tag (e.g., 'Lead from Website'), and adds them to a follow-up email sequence. The form data goes through a Next.js API route that calls Keap's contact creation endpoint with the form fields.
Build a lead capture form that creates a Keap contact on submission. Create a server action that calls POST https://api.infusionsoft.com/crm/rest/v2/contacts with the Keap API token from process.env.KEAP_ACCESS_TOKEN. The request body should include: given_name, family_name, email_addresses (array with field email_address and field type EMAIL1), and phone_numbers (array with field number and field type PHONE1). After creating the contact, apply tag ID 123 by calling POST /contacts/{id}/tags. Show a success message after submission. Handle duplicate email errors gracefully.
Copy this prompt to try it in Bolt.new
Keap + Twilio Small Business Communication Hub
A dashboard that combines Keap contact data with Twilio SMS capabilities. The user searches for a Keap contact, views their deal history and notes, and can send an SMS message directly from the interface via Twilio. Keap provides the contact data and deal context; Twilio handles the outbound SMS.
Build a contact communication page. Fetch a contact from Keap by email using GET https://api.infusionsoft.com/crm/rest/v2/contacts?email={email} via a Next.js API route. Display the contact's name, company, deals, and tags. Add an SMS button that opens a message input and calls a separate /api/twilio/sms API route to send the message via Twilio to the contact's phone number. Store KEAP_ACCESS_TOKEN and TWILIO_AUTH_TOKEN in .env. Show message send confirmation.
Copy this prompt to try it in Bolt.new
Troubleshooting
The OAuth callback returns 'invalid_grant' or 'invalid_client' when exchanging the authorization code
Cause: The authorization code has expired (codes are valid for only a few minutes and can only be used once), the redirect_uri in the token exchange does not exactly match the redirect_uri registered in the Keap developer portal, or the client credentials (Client ID and Client Secret) are wrong.
Solution: Verify KEAP_CLIENT_ID, KEAP_CLIENT_SECRET, and KEAP_REDIRECT_URI in your .env match exactly what is configured in developer.keap.com. The redirect_uri must be an exact string match including https:// and trailing slash (or lack thereof). If the code expired, restart the authorization flow by visiting /api/auth/keap again. Check that your Keap developer app is not in 'development mode' which restricts certain behaviors.
Keap API returns 401 Unauthorized on contacts requests even though authorization completed successfully
Cause: The access token has expired (Keap access tokens expire after approximately 24 hours) and the token refresh has not been implemented, the access token cookie has expired or been cleared, or the token was revoked by the user in their Keap account settings.
Solution: Implement token refresh logic: when a 401 response is received, use the refresh token to get a new access token from the Keap token endpoint with grant_type=refresh_token. Update the stored token. If the refresh token is also expired or invalid, redirect the user to re-authorize via /api/auth/keap. For production apps, implement proactive token refresh (refresh before the token expires rather than reacting to 401 errors).
1// Token refresh utility2async function refreshKeapToken(refreshToken: string): Promise<KeapTokenResponse> {3 const credentials = Buffer.from(4 `${process.env.KEAP_CLIENT_ID}:${process.env.KEAP_CLIENT_SECRET}`5 ).toString('base64');6 7 const response = await fetch(KEAP_TOKEN_URL, {8 method: 'POST',9 headers: { 'Authorization': `Basic ${credentials}`, 'Content-Type': 'application/x-www-form-urlencoded' },10 body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }),11 });12 if (!response.ok) throw new Error('Token refresh failed — user must re-authorize');13 return response.json();14}Keap OAuth redirect fails in the Bolt WebContainer preview with a redirect_uri mismatch error
Cause: The Bolt WebContainer preview uses a dynamic URL that changes each session (e.g., https://hash.local.webcontainer-api.io/). This URL cannot be registered as an OAuth redirect URI in the Keap developer portal, and even if it could be, it would change with each Bolt session.
Solution: Deploy the app to Netlify or Bolt Cloud first to get a stable HTTPS URL. Register that deployed URL as the redirect URI in the Keap developer portal (e.g., https://yourapp.netlify.app/api/auth/keap/callback). Update KEAP_REDIRECT_URI in your hosting platform's environment variables. The OAuth flow can only be tested after deployment — this is a fundamental constraint of OAuth with Bolt's WebContainer, not a bug in your code.
Creating a contact returns 422 Unprocessable Entity with an error about duplicate email
Cause: A contact with the submitted email address already exists in the Keap account. Keap enforces unique email addresses across contacts — you cannot create two contacts with the same email.
Solution: Handle 422 responses in your contact creation code as 'already exists' rather than a failure. Search for the existing contact first using GET /contacts?email={email}, then decide whether to update the existing contact or return a success message to the user. The upsert pattern (search-then-create-or-update) is the standard approach for form-based contact creation where duplicate submissions are common.
1// Check for existing contact before creating2const searchRes = await fetch(`${KEAP_API_BASE}/contacts?email=${encodeURIComponent(email)}`, {3 headers: { Authorization: `Bearer ${token}` },4});5const { contacts } = await searchRes.json();6if (contacts && contacts.length > 0) {7 // Contact exists — update instead of create8 const existingId = contacts[0].id;9 await fetch(`${KEAP_API_BASE}/contacts/${existingId}`, { method: 'PATCH', ... });10}Best practices
- Use a Keap Service Account Key for server-to-server integrations (landing page lead capture) and OAuth for user-facing multi-account tools — service account keys never expire and are simpler to manage for single-account use cases
- Always store Keap OAuth tokens (access and refresh) in HTTP-only cookies or an encrypted database field, never in localStorage or non-httpOnly cookies where JavaScript can access them
- Implement proactive token refresh: check token expiry before every API call and refresh if it expires within 5 minutes — reacting to 401 errors causes visible failures for users
- Store frequently used tag IDs and sequence IDs as environment variables rather than hardcoding them — Keap tag IDs can differ between development and production Keap accounts
- Use Keap's contact search endpoint (GET /contacts?email=) before creating to avoid 422 duplicate errors, especially on high-traffic forms where the same user might submit multiple times
- For phone and SMS features alongside Keap contact data, use Twilio — Keap's built-in business phone is not accessible through the API and has no programmatic interface for outbound calls or SMS
- Test your OAuth flow and API calls on a deployed environment (Netlify or Bolt Cloud) rather than the Bolt preview — OAuth callbacks cannot work with Bolt's dynamic WebContainer URLs
Alternatives
Choose HubSpot if you need a more feature-rich CRM API with better documentation, a free tier with API access, and a larger ecosystem of integrations — HubSpot's API is significantly easier to work with than Keap's for complex use cases.
Choose Pipedrive if your focus is sales pipeline management with a simpler API using API key authentication rather than OAuth — Pipedrive's API is easier to prototype with in Bolt.new during development.
Choose Zoho CRM if you need a more affordable all-in-one business platform with a comprehensive REST API and a free developer tier for testing integrations.
Use the Infusionsoft page if you are working with the legacy Keap Classic product — the same platform under the old branding with the original XML-RPC and legacy REST API endpoints.
Frequently asked questions
Can I use Keap phone features through the Bolt.new integration?
Keap's REST API v2 does not expose the built-in business phone features — call logs, voicemail, and SMS sending through Keap's phone system are not available programmatically. To add phone and SMS capabilities to your Bolt.new app alongside Keap contact data, integrate Twilio separately. Twilio's API provides outbound SMS, voice calls, and call tracking that can be linked to Keap contact records using the contact's phone number as the common identifier.
Does Keap integration work in Bolt.new's WebContainer preview?
Partially. API calls to Keap's endpoints work in the preview once you have a valid access token stored — outbound HTTP calls from Next.js API routes work in the WebContainer. However, the OAuth authorization flow (which generates the access token) cannot complete in the preview because Keap needs to redirect to a stable callback URL, which the WebContainer's dynamic session URL cannot provide. Complete the OAuth flow once on your deployed app, then use the resulting token in development.
What is the difference between Keap and Infusionsoft?
Keap and Infusionsoft are the same platform. In 2019, Infusionsoft rebranded to Keap and split the product into Keap (simplified small business CRM) and kept the Infusionsoft name for the legacy enterprise product. As of 2022, both are called Keap with different plan tiers. If you are on a plan from before 2020 with an Infusionsoft URL, use the legacy XML-RPC or v1 REST API. If you signed up recently, use Keap REST API v2 as covered in this guide.
Do I need a paid Keap plan to access the API?
Yes. Keap REST API v2 access requires the Keap Pro plan ($159/month) or higher. The Keap Lite plan ($79/month) has limited or no API access. Check your account at app.keap.com under Settings → Integrations to confirm API access is available. If you are building a tool for a client, they need to be on a qualifying plan for you to make API calls on their behalf.
How do I deploy a Bolt.new app with Keap OAuth to Netlify?
Push your code to GitHub and connect the repo to Netlify, or use Bolt's built-in Netlify integration under Settings → Applications. In Netlify's site settings under Environment Variables, add KEAP_CLIENT_ID, KEAP_CLIENT_SECRET, and KEAP_REDIRECT_URI (set to https://yourapp.netlify.app/api/auth/keap/callback). Update the redirect URI in your Keap developer portal at developer.keap.com to match. Then trigger a fresh OAuth authorization on the deployed site to generate working tokens.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation