Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Keap

Integrate Keap (formerly Infusionsoft) with Bolt.new using their REST API v1 or the legacy XML-RPC API. Use OAuth 2.0 for authentication — get your Client ID and Client Secret from the Keap Developer Portal, implement the authorization code flow, and store the access token in your environment variables for API calls. All Keap API calls must go through a Next.js API route. OAuth callback flows and webhooks require a deployed Netlify URL.

What you'll learn

  • How to register a Keap OAuth app and obtain API credentials from the Keap Developer Portal
  • How to implement the OAuth 2.0 authorization code flow for Keap authentication in a Next.js app
  • How to create and update contacts in Keap from Bolt.new form submissions
  • How to apply Keap tags to contacts based on user actions to trigger automation sequences
  • How to build a contact and pipeline management dashboard using the Keap REST API
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate17 min read30 minutesMarketingApril 2026RapidDev Engineering Team
TL;DR

Integrate Keap (formerly Infusionsoft) with Bolt.new using their REST API v1 or the legacy XML-RPC API. Use OAuth 2.0 for authentication — get your Client ID and Client Secret from the Keap Developer Portal, implement the authorization code flow, and store the access token in your environment variables for API calls. All Keap API calls must go through a Next.js API route. OAuth callback flows and webhooks require a deployed Netlify URL.

Connect Bolt.new to Keap CRM and Marketing Automation

Keap, rebranded from Infusionsoft in 2019, is a small business-focused CRM and marketing automation platform with a particular strength in service-based businesses — coaches, consultants, agencies, and local service providers. Its tag-based automation system is powerful: you assign tags to contacts, and automation sequences (called 'Campaigns' in Keap's terminology) trigger based on tag assignment. This allows highly targeted email sequences, task creation, and sales pipeline movements driven by contact behavior and status changes.

Keap's authentication model is more complex than most platforms in this tier — it uses OAuth 2.0, which means you need to register a developer application, implement an authorization flow, and manage token refresh. This added complexity is worth understanding before starting: for a personal integration where you are connecting your own Keap account, you can obtain a long-lived access token once and store it. For a multi-tenant product where different clients connect their own Keap accounts, you need to implement the full OAuth flow with per-user token storage.

For Bolt.new developers, the most important constraint is that OAuth callback URLs must be stable, registered URIs — which rules out Bolt's WebContainer preview URL. Deploy to Netlify first, register your Netlify URL as the OAuth redirect URI in the Keap Developer Portal, then implement and test the OAuth flow. Once you have a working access token, contact management and tag assignment via the REST API work seamlessly from Next.js API routes.

Integration method

Bolt Chat + API Route

Keap's REST API v1 uses OAuth 2.0 with an authorization code flow for authentication. You need a Keap Developer account, a registered OAuth app with Client ID and Client Secret, and an access token obtained through the OAuth flow. All API calls go through Next.js API routes using the stored access token in server-side environment variables. The OAuth callback flow requires a deployed URL — configure your Keap OAuth app's redirect URI to use your Netlify domain.

Prerequisites

  • A Keap account — API access requires a Keap Pro or Max plan; the free/starter plans do not include API access
  • A Keap Developer account at keys.developer.keap.com — register your app to get Client ID and Client Secret
  • An OAuth access token — obtained through Keap's OAuth 2.0 flow; for personal use, get a key directly from the Developer Portal
  • Your Keap API base URL — typically https://api.infusionsoft.com/crm/rest or https://api.infusionsoft.com (check your Keap account region)
  • A Netlify account for deployment — the OAuth callback URL must be a deployed domain registered in your Keap OAuth app settings

Step-by-step guide

1

Register a Keap Developer app and obtain API credentials

Keap uses OAuth 2.0 for API authentication, which requires registering an application in the Keap Developer Portal before making any API calls. This is more involved than the API key approach used by many other platforms, but it is a one-time setup. Start by going to the Keap Developer Portal at developer.keap.com and signing in with your Keap account. Go to your Apps and create a new application. Provide an app name, description, and most importantly, the OAuth redirect URI — this is the URL Keap will redirect to after a user authorizes your app. For development testing, use your Netlify URL (https://your-app.netlify.app/api/keap/callback) since the WebContainer preview URL cannot be registered. After creating the app, note the Client ID and Client Secret from the app settings page. These are permanent credentials for your app — keep the Client Secret server-side only. For a personal integration (connecting your own single Keap account), there is a shortcut: in the Developer Portal, you can generate a personal access token directly without going through the full OAuth flow. This token is long-lived and can be stored in KEAP_ACCESS_TOKEN for direct use in API calls. This approach works for integrations where one developer connects their own Keap account. For multi-tenant use (different users connect their own Keap accounts), you must implement the full OAuth flow: redirect users to Keap's authorization URL, receive the authorization code at your callback endpoint, exchange it for an access token via POST to Keap's token endpoint, and store the access token per user. Keap access tokens expire — implement refresh token rotation to maintain access.

Bolt.new Prompt

Set up Keap API integration. Create .env.local with: KEAP_CLIENT_ID=your-client-id, KEAP_CLIENT_SECRET=your-client-secret, KEAP_ACCESS_TOKEN=your-personal-access-token (for single-account use), KEAP_API_URL=https://api.infusionsoft.com/crm/rest. Create lib/keap.ts with: a base keapRequest function using KEAP_ACCESS_TOKEN as Bearer token, TypeScript interfaces for KeapContact (id, given_name, family_name, email_addresses array with field/email, phone_numbers array with field/number, tag_ids number array), and exported functions: createContact, updateContact, findContactByEmail, applyTag, and createNote.

Paste this in Bolt.new chat

lib/keap.ts
1// lib/keap.ts
2const API_URL = process.env.KEAP_API_URL || 'https://api.infusionsoft.com/crm/rest';
3
4export interface KeapEmailAddress {
5 field: 'EMAIL1' | 'EMAIL2' | 'EMAIL3';
6 email: string;
7}
8
9export interface KeapPhoneNumber {
10 field: 'PHONE1' | 'PHONE2' | 'PHONE3';
11 number: string;
12 type?: string;
13}
14
15export interface KeapContact {
16 id?: number;
17 given_name?: string;
18 family_name?: string;
19 email_addresses?: KeapEmailAddress[];
20 phone_numbers?: KeapPhoneNumber[];
21 tag_ids?: number[];
22 source_type?: string;
23 lead_source_id?: number;
24 custom_fields?: Array<{ id: number; content: unknown }>;
25}
26
27async function keapRequest<T>(
28 path: string,
29 method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
30 body?: unknown
31): Promise<T> {
32 const token = process.env.KEAP_ACCESS_TOKEN;
33 if (!token) throw new Error('KEAP_ACCESS_TOKEN is not set');
34
35 const response = await fetch(`${API_URL}${path}`, {
36 method,
37 headers: {
38 'Authorization': `Bearer ${token}`,
39 'Content-Type': 'application/json',
40 },
41 ...(body ? { body: JSON.stringify(body) } : {}),
42 });
43
44 if (response.status === 204) return null as T;
45 const data = await response.json();
46 if (!response.ok) {
47 throw new Error(data.message || `Keap API error: ${response.status}`);
48 }
49 return data as T;
50}
51
52export async function findContactByEmail(email: string): Promise<KeapContact | null> {
53 const result = await keapRequest<{ contacts: KeapContact[] }>(
54 `/v1/contacts?email=${encodeURIComponent(email)}&fields=id,given_name,family_name,email_addresses,tag_ids`
55 );
56 return result.contacts?.[0] || null;
57}
58
59export async function createContact(contact: KeapContact): Promise<KeapContact> {
60 return keapRequest<KeapContact>('/v1/contacts', 'POST', contact);
61}
62
63export async function updateContact(id: number, data: Partial<KeapContact>): Promise<KeapContact> {
64 return keapRequest<KeapContact>(`/v1/contacts/${id}`, 'PATCH', data);
65}
66
67export async function applyTag(contactId: number, tagId: number): Promise<void> {
68 await keapRequest(`/v1/contacts/${contactId}/tags`, 'POST', { tagIds: [tagId] });
69}
70
71export async function createNote(
72 contactId: number,
73 title: string,
74 body: string
75): Promise<void> {
76 await keapRequest('/v1/notes', 'POST', {
77 contact_id: contactId,
78 title,
79 body,
80 type: 'Other',
81 });
82}

Pro tip: For a personal Keap integration (single account), use the personal access token from the Keap Developer Portal directly as KEAP_ACCESS_TOKEN. Personal access tokens are long-lived and simplify the setup significantly. Only implement the full OAuth flow if you need multiple users to connect their own Keap accounts.

Expected result: A lib/keap.ts helper with typed functions for contact management, tag assignment, and note creation, with API credentials in .env.local.

2

Build the contact creation and tagging API route

The contact creation route handles the most common Keap integration pattern: capturing a lead from a form submission and creating a corresponding Keap contact, then applying one or more tags to trigger automation sequences. Keap contacts have a specific structure for email addresses and phone numbers — both use arrays of objects with a `field` identifier (`EMAIL1`, `EMAIL2` for emails; `PHONE1`, `PHONE2` for phones) and the actual value. This allows storing multiple emails and phone numbers per contact. When creating contacts, always use `EMAIL1` as the primary email field. The duplicate contact challenge: Keap has built-in deduplication that attempts to merge contacts with the same email, but behavior depends on your Keap deduplication settings. Before creating a new contact, search for an existing one using `findContactByEmail()`. If found, update the existing record rather than creating a new one. This is especially important for scenarios where the same person might submit multiple forms. Tag IDs are numeric integers in Keap. Find tag IDs in Keap under Contacts → Tags, then click on a tag — the ID appears in the URL. Store commonly used tag IDs as environment variables (KEAP_NEW_LEAD_TAG_ID, KEAP_CUSTOMER_TAG_ID, etc.) to avoid hardcoding numeric values in your code. After applying a tag in Keap, any automation sequences that trigger on that tag assignment begin running automatically. This is the core mechanism for starting email sequences from your Bolt app — you do not call Keap's email API directly; you apply a tag and Keap handles the rest.

Bolt.new Prompt

Create a Keap lead capture API route at app/api/keap/leads/route.ts using lib/keap.ts. Accept POST with: firstName (required), lastName (required), email (required, validate format), phone (optional), source (optional). Check for existing contact by email using findContactByEmail. If found, update with any new data. If new, create contact with email_addresses: [{field: 'EMAIL1', email}], phone_numbers if provided, and given_name/family_name. After create/update, apply tag from KEAP_NEW_LEAD_TAG_ID env var using applyTag. Return 201 for new, 200 for update, with contactId. Handle errors with 500. Create a LeadForm React component with name, email, phone fields and success message.

Paste this in Bolt.new chat

app/api/keap/leads/route.ts
1// app/api/keap/leads/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { findContactByEmail, createContact, updateContact, applyTag } from '@/lib/keap';
4
5const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
7export async function POST(request: NextRequest) {
8 const body = await request.json();
9 const { firstName, lastName, email, phone, source } = body;
10
11 if (!firstName?.trim()) return NextResponse.json({ error: 'First name is required' }, { status: 400 });
12 if (!lastName?.trim()) return NextResponse.json({ error: 'Last name is required' }, { status: 400 });
13 if (!email || !emailRegex.test(email)) {
14 return NextResponse.json({ error: 'Valid email is required' }, { status: 400 });
15 }
16
17 const contactData = {
18 given_name: firstName.trim(),
19 family_name: lastName.trim(),
20 email_addresses: [{ field: 'EMAIL1' as const, email: email.toLowerCase().trim() }],
21 ...(phone ? { phone_numbers: [{ field: 'PHONE1' as const, number: phone.trim(), type: 'Work' }] } : {}),
22 ...(source ? { source_type: source } : {}),
23 };
24
25 try {
26 const existing = await findContactByEmail(email);
27 let contactId: number;
28 let action: 'created' | 'updated';
29
30 if (existing?.id) {
31 await updateContact(existing.id, contactData);
32 contactId = existing.id;
33 action = 'updated';
34 } else {
35 const created = await createContact(contactData);
36 contactId = created.id!;
37 action = 'created';
38 }
39
40 // Apply new lead tag to trigger Keap automation
41 const tagId = process.env.KEAP_NEW_LEAD_TAG_ID;
42 if (tagId && contactId) {
43 try {
44 await applyTag(contactId, parseInt(tagId, 10));
45 } catch (tagError) {
46 console.error('Keap tag application failed (non-critical):', tagError);
47 }
48 }
49
50 const statusCode = action === 'created' ? 201 : 200;
51 return NextResponse.json({ success: true, action, contactId }, { status: statusCode });
52 } catch (error) {
53 console.error('Keap lead creation error:', error);
54 return NextResponse.json({ error: 'Failed to save contact' }, { status: 500 });
55 }
56}

Pro tip: Keap tag IDs are integers. Find them in your Keap account under Contacts → Tags — the ID is visible in the URL when you click on a tag (e.g., .../tags/1234/details where 1234 is the tag ID). Store frequently used tag IDs as environment variables like KEAP_NEW_LEAD_TAG_ID=1234 to avoid hardcoding numeric IDs in your code.

Expected result: A contact creation API route that finds or creates Keap contacts from form submissions and applies tags to trigger automation sequences.

3

Implement OAuth flow for multi-tenant Keap connections

If your Bolt app needs multiple users to connect their own Keap accounts (a SaaS tool that integrates with clients' Keap), you need to implement the OAuth 2.0 authorization code flow. This is different from the personal access token approach — it involves redirecting users to Keap's authorization page, receiving a code at your callback URL, and exchanging that code for access and refresh tokens. The OAuth flow requires two routes: an authorization route that redirects the user to Keap's OAuth page, and a callback route that handles the redirect from Keap with the authorization code. The callback route exchanges the code for tokens and stores them. Keap's OAuth endpoints: authorization URL is `https://accounts.infusionsoft.com/app/oauth/authorize`, token URL is `https://api.infusionsoft.com/token`. Parameters follow standard OAuth 2.0 conventions. The redirect URI in your authorization request must exactly match the URI registered in your Keap Developer app settings. Keap access tokens expire and must be refreshed using the refresh token. When an API call returns 401, refresh the access token using the refresh token endpoint and retry the call. For Supabase-backed apps, store the access token, refresh token, and expiry timestamp per user in a Supabase table. This is the most complex part of a Keap integration. For single-account (personal) integrations, use the personal access token approach from Step 1 instead. This step is only needed for multi-tenant SaaS products.

Bolt.new Prompt

Create OAuth routes for Keap. Create app/api/keap/auth/route.ts that generates the Keap OAuth authorization URL with KEAP_CLIENT_ID, redirect_uri pointing to /api/keap/callback, response_type=code, and scope=full. Redirect the user to this URL. Create app/api/keap/callback/route.ts that accepts the code param from Keap's redirect, POSTs to https://api.infusionsoft.com/token with grant_type=authorization_code, client_id, client_secret, redirect_uri, and code to exchange for access_token and refresh_token. Store both tokens in a Supabase keap_tokens table with user_id, access_token, refresh_token, expires_at. Redirect to /dashboard on success. Add a ConnectKeap button component that links to /api/keap/auth.

Paste this in Bolt.new chat

app/api/keap/callback/route.ts
1// app/api/keap/callback/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { createClient } from '@supabase/supabase-js';
4
5export async function GET(request: NextRequest) {
6 const { searchParams } = new URL(request.url);
7 const code = searchParams.get('code');
8 const error = searchParams.get('error');
9
10 if (error || !code) {
11 return NextResponse.redirect(new URL('/settings?error=keap_auth_failed', request.url));
12 }
13
14 const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/keap/callback`;
15
16 const tokenResponse = await fetch('https://api.infusionsoft.com/token', {
17 method: 'POST',
18 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
19 body: new URLSearchParams({
20 grant_type: 'authorization_code',
21 client_id: process.env.KEAP_CLIENT_ID!,
22 client_secret: process.env.KEAP_CLIENT_SECRET!,
23 redirect_uri: redirectUri,
24 code,
25 }),
26 });
27
28 if (!tokenResponse.ok) {
29 console.error('Keap token exchange failed:', await tokenResponse.text());
30 return NextResponse.redirect(new URL('/settings?error=keap_token_failed', request.url));
31 }
32
33 const tokens = await tokenResponse.json();
34 const expiresAt = new Date(Date.now() + tokens.expires_in * 1000).toISOString();
35
36 const supabase = createClient(
37 process.env.NEXT_PUBLIC_SUPABASE_URL!,
38 process.env.SUPABASE_SERVICE_ROLE_KEY!
39 );
40
41 // Store tokens — in production, associate with the current user's ID
42 await supabase.from('keap_tokens').upsert({
43 access_token: tokens.access_token,
44 refresh_token: tokens.refresh_token,
45 expires_at: expiresAt,
46 }, { onConflict: 'user_id' }); // adjust based on your auth model
47
48 return NextResponse.redirect(new URL('/dashboard?connected=keap', request.url));
49}

Pro tip: The OAuth callback URL registered in your Keap Developer app must exactly match the redirect_uri you send in OAuth requests. During development in Bolt's WebContainer, the preview URL is not a stable public address and cannot be registered. Deploy to Netlify first, register https://your-app.netlify.app/api/keap/callback as the redirect URI in the Keap Developer Portal, then test the OAuth flow on your deployed site.

Expected result: A working Keap OAuth flow that redirects users to Keap authorization and stores access tokens in Supabase after the callback.

4

Deploy to Netlify and complete OAuth configuration

Deploying to Netlify is essential for Keap integrations — the OAuth callback URL must be a deployed, publicly accessible URL registered in the Keap Developer Portal. The Bolt WebContainer preview URL changes per session and cannot be used as a registered OAuth redirect URI. To deploy: click Deploy in Bolt.new, connect to Netlify via OAuth, and wait for the initial build. After deploying, your app will have a stable `*.netlify.app` URL. In the Keap Developer Portal, update your app's OAuth Redirect URI to include your Netlify URL (e.g., `https://your-app.netlify.app/api/keap/callback`). In the Netlify dashboard → Site Configuration → Environment Variables, add: KEAP_CLIENT_ID, KEAP_CLIENT_SECRET, KEAP_ACCESS_TOKEN (if using personal token), KEAP_API_URL, KEAP_NEW_LEAD_TAG_ID, and any other tag IDs you reference. Also add NEXT_PUBLIC_APP_URL with your Netlify URL — this is used for constructing the OAuth redirect URI in your callback route. Trigger a redeploy. For Keap webhooks: Keap supports webhooks (called 'Hook Types' in their developer documentation) for contact events. Configure them via the Keap API itself: `POST /v1/hooks` with the hookTypeKey, eventKey, objectKey (contact ID), and your callback URL. Keap webhooks are configured programmatically via the API rather than through a UI settings page. Remember: webhook delivery and OAuth callbacks cannot work in Bolt's WebContainer. Both require a deployed, publicly accessible URL. Deploy to Netlify early in your development process to enable these features.

Bolt.new Prompt

Add netlify.toml with Next.js 14+ build configuration. Also create a Keap connection status API route at app/api/keap/status/route.ts that: calls GET /v1/account/profile using the KEAP_ACCESS_TOKEN, returns isConnected: true with accountName if successful, returns isConnected: false if the token is missing or invalid (401). Create a KeapConnectionStatus React component that calls this route on mount, shows a green 'Connected to Keap: {accountName}' badge if connected, or an amber 'Not connected' badge with a 'Connect Keap' button linking to /api/keap/auth if not connected.

Paste this in Bolt.new chat

netlify.toml
1# netlify.toml
2[build]
3 command = "npm run build"
4 publish = ".next"
5
6[[plugins]]
7 package = "@netlify/plugin-nextjs"
8
9[build.environment]
10 NODE_VERSION = "20"
11 NEXT_TELEMETRY_DISABLED = "1"

Pro tip: After deploying, test the Keap connection using your app's status endpoint before testing the full OAuth or API flow. If the status endpoint returns isConnected: true, your API credentials work. If it returns 401, the access token has expired or is incorrect — regenerate a personal token or re-authorize the OAuth app.

Expected result: A deployed Netlify app with working Keap API integration, OAuth redirect URI registered in the Keap Developer Portal, and environment variables configured.

Common use cases

Lead Capture Form to Keap CRM

Capture leads from a landing page form into Keap, assign relevant tags that trigger automation sequences, and optionally assign the lead to a pipeline stage. The contact is created immediately in Keap, ensuring no leads are lost and your automated follow-up sequences start right away.

Bolt.new Prompt

Create a lead capture form that sends contacts to Keap. Build an API route at app/api/keap/contacts/route.ts using KEAP_ACCESS_TOKEN and KEAP_API_URL env vars. Accept POST with firstName, lastName, email, phone, source. Use Keap REST API POST /v1/contacts to create the contact, mapping fields to given_name, family_name, email_addresses (array), phone_numbers (array). After creating, apply a tag using POST /v1/contacts/{id}/tags with KEAP_NEW_LEAD_TAG_ID env var. Return 201 with the contact ID. Create a LeadCaptureForm React component with validation and success message.

Copy this prompt to try it in Bolt.new

Appointment Booking Confirmation to Keap

After a user books an appointment (via Calendly, your own booking system, etc.), update their Keap contact record with the appointment details, apply a 'booked' tag to trigger a preparation email sequence, and log an activity noting the appointment date. Keeps the sales team informed of upcoming meetings directly in Keap.

Bolt.new Prompt

Create an appointment booking sync. Build an API route at app/api/keap/appointment/route.ts that accepts POST with contactEmail, appointmentDate, appointmentType. First GET /v1/contacts?email={email} to find the existing contact or create one. Apply tag from KEAP_APPOINTMENT_TAG_ID. Log a note using POST /v1/contacts/{id}/notes with body showing appointment type and date. Update the contact's custom field for next_appointment_date using KEAP_APPOINTMENT_FIELD_ID. Return the Keap contact ID.

Copy this prompt to try it in Bolt.new

Purchase-Triggered Keap Automation

After a Stripe payment completes, create or update the customer contact in Keap, apply a buyer tag to trigger a post-purchase automation sequence, and log the order details as a note. The automation sequence handles welcome emails, upsell sequences, and review requests automatically.

Bolt.new Prompt

Integrate Keap with Stripe checkout. In my Stripe webhook handler at app/api/stripe/webhook/route.ts, after checkout.session.completed, call a syncToKeap(email, name, amount, productName) function. This function should: search for existing contact by email using GET /v1/contacts?email={email}, create if not found using POST /v1/contacts, apply the buyer tag using KEAP_BUYER_TAG_ID, and log a purchase note. Wrap all Keap calls in try-catch so Stripe processing continues if Keap fails.

Copy this prompt to try it in Bolt.new

Troubleshooting

Keap API returns 401 Unauthorized even with the correct access token

Cause: Keap access tokens expire, typically after a few hours for OAuth-issued tokens. Personal access tokens from the Developer Portal are long-lived but can also expire or be revoked. The token may also be stored incorrectly — extra spaces or newlines in the environment variable value cause authentication failures.

Solution: Regenerate a personal access token from the Keap Developer Portal if using personal tokens. For OAuth tokens, implement refresh token rotation: catch 401 errors, use the refresh token to get a new access token from https://api.infusionsoft.com/token, update the stored token, and retry the failed request.

typescript
1// Refresh token flow
2async function refreshKeapToken(refreshToken: string) {
3 const response = await fetch('https://api.infusionsoft.com/token', {
4 method: 'POST',
5 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
6 body: new URLSearchParams({
7 grant_type: 'refresh_token',
8 refresh_token: refreshToken,
9 client_id: process.env.KEAP_CLIENT_ID!,
10 client_secret: process.env.KEAP_CLIENT_SECRET!,
11 }),
12 });
13 return response.json();
14}

OAuth callback fails with 'redirect_uri mismatch' error

Cause: The redirect_uri in your OAuth request does not exactly match the URI registered in the Keap Developer Portal. Any difference — trailing slash, HTTP vs HTTPS, different domain — causes this error.

Solution: In the Keap Developer Portal, verify the exact redirect URI registered for your app. Ensure NEXT_PUBLIC_APP_URL in your environment variables matches your deployed Netlify domain exactly (no trailing slash). The redirect_uri sent in the authorization request must be character-for-character identical to the registered URI.

Tag application succeeds (no error) but Keap automations are not triggering

Cause: The tag was applied but the automation sequence trigger does not match, or the automation is not published/active. Keap automations must be in 'Published' status to run.

Solution: In Keap, go to Marketing → Campaigns → select the automation. Check that the trigger is 'Tag is applied' and the tag matches exactly. Also verify the campaign is in 'Published' status (not 'Draft'). Check the automation's goal settings to ensure it fires for new tag applications and is not limited to contacts in a specific state.

OAuth flow cannot be tested in Bolt.new's WebContainer preview

Cause: OAuth callback URLs must be stable, registered URIs. Bolt's WebContainer generates a dynamic preview URL that changes per session and cannot be registered as an OAuth redirect URI in the Keap Developer Portal.

Solution: Deploy to Netlify first. Register https://your-app.netlify.app/api/keap/callback as the redirect URI in your Keap OAuth app settings. Test all OAuth functionality on the deployed Netlify site. This is a fundamental WebContainer limitation that affects all OAuth flows requiring registered redirect URIs.

Best practices

  • Use a personal access token from the Keap Developer Portal for single-account integrations — it avoids implementing OAuth token refresh and is sufficient for connecting one Keap account
  • Store Keap tag IDs as environment variables (KEAP_NEW_LEAD_TAG_ID, KEAP_CUSTOMER_TAG_ID) rather than hardcoding numeric IDs — tag IDs can change if tags are deleted and recreated
  • Always implement the create-or-update pattern for contacts — search by email before creating to prevent duplicate records that are difficult to merge in Keap
  • Wrap Keap API calls in try-catch with non-throwing error handling when calling from Stripe webhooks or signup flows — a Keap API failure should never break core user flows
  • Deploy to Netlify before implementing OAuth flows — the callback URL must be a deployed, registered domain; the Bolt WebContainer preview cannot be used for OAuth redirect URIs
  • Implement token refresh logic for OAuth tokens — Keap access tokens expire, and handling 401 errors with automatic refresh avoids users losing their Keap connection
  • Test API calls in Bolt.new preview before deploying — contact creation and tag assignment using a personal access token work in the WebContainer since they are outbound HTTP requests
  • Verify tag trigger automations are Published in Keap after applying tags — Draft campaigns do not run even when the trigger tag is applied

Alternatives

Frequently asked questions

Can I use Keap's API without implementing OAuth?

Yes, for single-account integrations. The Keap Developer Portal allows generating a personal access token that can be used directly as a Bearer token in API calls without going through the OAuth authorization code flow. This is the recommended approach for personal integrations or when building tools that connect a single specific Keap account. OAuth is only required for multi-tenant apps where different users connect their own Keap accounts.

Why does Keap's API documentation still reference Infusionsoft?

Keap rebranded from Infusionsoft in 2019, but many API endpoints, documentation pages, and SDK references still use the Infusionsoft name. The API domain is api.infusionsoft.com, the OAuth token URL is api.infusionsoft.com/token, and older documentation refers to the product as Infusionsoft. This is expected — the API has not changed, only the brand name.

Can Keap API calls work in Bolt.new's WebContainer during development?

Outbound API calls (creating contacts, applying tags, fetching data) using a personal access token work in the WebContainer — they are standard HTTPS requests. OAuth callback flows cannot work in the WebContainer because the callback URL must be a stable, registered domain. Deploy to Netlify first before testing OAuth flows.

What Keap plan is required for API access?

Keap API access requires a Keap Pro or Max plan. The basic Keap Grow plan (now called Keap) does not include API access. Check your plan details at keap.com/pricing — API is listed under the Pro plan and above. If your account does not have API access, upgrading to Pro is required before any integration can work.

How do Keap tags work as automation triggers?

In Keap, you create Campaign automation sequences with a 'Tag is applied' goal as the entry trigger. When your API applies that specific tag to a contact, Keap immediately enrolls that contact in the campaign sequence. The sequence can then send emails, create tasks, apply additional tags, update custom fields, or move the contact through a pipeline stage. Tags are the primary mechanism for triggering automation from external integrations.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.