# How to Integrate Lovable with Keap

- Tool: Lovable
- Difficulty: Intermediate
- Time required: 35 minutes
- Last updated: March 2026

## TL;DR

To integrate Keap with Lovable, register an application in the Keap Developer portal to get OAuth2 credentials, complete the authorization flow to obtain access and refresh tokens, store them in Cloud → Secrets, then build Edge Functions that call the Keap REST API v2 for contacts, deals, invoices, and automations. Keap's modern API v2 is simpler and more powerful than the legacy Infusionsoft/Keap Max Classic API.

## Connect Lovable to Keap's modern API v2 for small business CRM and invoicing automation

Keap is the modern evolution of the Infusionsoft platform — a streamlined CRM and business automation tool for small businesses and entrepreneurs. While Infusionsoft (now Keap Max Classic) is the legacy product with a complex XML-RPC API history, modern Keap (Keap Pro and Keap Max) offers a clean REST API v2 that is significantly easier to work with. For new Lovable integrations, Keap REST API v2 is the right choice over the legacy Infusionsoft API.

Keap's platform uniquely combines what most businesses need three separate tools for: CRM contacts and pipeline, invoicing and payment collection, and marketing automation with email sequences and text message campaigns. For small business founders building internal management tools or client portals in Lovable, Keap integration creates a single data layer across all these functions without requiring separate Stripe (for invoicing), Mailchimp (for email), and HubSpot (for CRM) integrations.

The authentication model is OAuth2 with authorization code flow — the same as Zoho CRM and Infusionsoft, using the Keap Developer portal at developer.infusionsoft.com (which serves both Keap and Infusionsoft integrations). Access tokens expire after 24 hours, and the Edge Function handles refresh automatically using the long-lived refresh token.

Keap REST API v2 base URL: https://api.infusionsoft.com/crm/rest/v2/ — note it still uses the infusionsoft.com domain despite being the modern Keap product. The v2 endpoints have a significantly improved design compared to v1, with better filtering, cleaner response structures, and more consistent naming conventions.

## Before you start

- A Lovable project with Lovable Cloud enabled (Edge Functions require a deployed app)
- An active Keap account (keap.com — Keap Pro at $159/month is the minimum paid tier for the modern Keap product; 14-day trial available)
- Access to the Keap Developer portal at developer.infusionsoft.com to register an OAuth2 application
- A tool for making HTTP requests to complete the OAuth2 token exchange (Postman, Insomnia, or Hoppscotch)
- Basic familiarity with Keap concepts: contacts, tags, opportunities (deals), invoices, and automation sequences

## Step-by-step guide

### 1. Register a Keap Developer application and obtain OAuth2 tokens

Keap uses OAuth2 authorization code flow for API authentication. Both modern Keap and legacy Infusionsoft integrations are registered at developer.infusionsoft.com.

Go to developer.infusionsoft.com and sign in with your Keap account. Click 'Register Application'. Fill in:
- Application Name: 'Lovable CRM Integration'
- Redirect URL: https://yourapp.lovable.app/oauth/callback
- Application Type: Server

After registration, you receive a Client ID and Client Secret.

Construct the OAuth2 authorization URL (note: Keap and Infusionsoft share the same OAuth2 endpoints):
https://accounts.infusionsoft.com/app/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&scope=full

Open this in your browser and authorize. Keap redirects to your redirect URI with a 'code' parameter. Copy the code — it expires in 10 minutes.

Exchange the code for tokens via POST to https://api.infusionsoft.com/token:
- grant_type: authorization_code
- client_id, client_secret, redirect_uri, code
- Send as form URL-encoded (application/x-www-form-urlencoded)

The response includes: access_token (valid 24 hours), refresh_token (expires after 90 days of non-use), and token_type: 'bearer'.

For Keap REST API v2 specifically: the API base URL is https://api.infusionsoft.com/crm/rest/v2/ (note v2, not v1). The v2 API has improved pagination, filtering, and response structures compared to the v1 API used by Infusionsoft.

**Expected result:** A Keap/Infusionsoft Developer application is registered. OAuth2 authorization code flow is completed. You have an access_token, refresh_token, Client ID, and Client Secret ready to store in Cloud → Secrets.

### 2. Store OAuth2 credentials in Cloud Secrets and note key Keap IDs

Add all Keap OAuth2 credentials to Lovable's Cloud Secrets panel. In Lovable, click '+' → Cloud panel → Secrets. Add:

- KEAP_CLIENT_ID — your application Client ID from the developer portal
- KEAP_CLIENT_SECRET — your application Client Secret
- KEAP_ACCESS_TOKEN — the current access token (24-hour lifetime)
- KEAP_REFRESH_TOKEN — the long-lived refresh token (90-day non-use expiry)

Keap API v2 uses the same OAuth2 token refresh endpoint as v1: POST https://api.infusionsoft.com/token with grant_type=refresh_token, client_id, client_secret, and refresh_token.

For tag-based automation, retrieve your tag IDs. In Keap, go to Settings → Tags or use the API: GET /crm/rest/v2/tags returns all tags with their numeric IDs and names. Store frequently-used tag IDs as additional secrets:
- KEAP_TAG_NEW_CONTACT — applied on signup
- KEAP_TAG_CUSTOMER — applied on first purchase

Keap API v2 uses slightly different field names than v1. Contact creation in v2 uses: given_name (first name), family_name (last name), email_addresses (array with { email, field: 'EMAIL1' }). Verify field names against the Keap API v2 documentation.

The Keap API v2 supports more powerful filtering than v1: GET /crm/rest/v2/contacts?filter=email==user@example.com uses equality filter syntax.

**Expected result:** KEAP_CLIENT_ID, KEAP_CLIENT_SECRET, KEAP_ACCESS_TOKEN, KEAP_REFRESH_TOKEN, and any tag ID secrets appear in Cloud → Secrets.

### 3. Create the Keap REST API v2 Edge Function with token refresh

Build the Edge Function for Keap REST API v2 operations. The v2 API has a cleaner design than v1, with consistent response shapes and better filtering support.

Keap REST API v2 base URL: https://api.infusionsoft.com/crm/rest/v2/

Key v2 endpoints:
- GET /contacts — list contacts with filter support
- POST /contacts — create contact
- PATCH /contacts/{id} — update contact
- POST /contacts/{contactId}/tags — apply tags
- DELETE /contacts/{contactId}/tags/{tagId} — remove a tag
- GET /invoices — list invoices
- POST /invoices — create invoice
- GET /opportunities — list opportunities (deals)
- POST /opportunities — create opportunity

The token refresh uses the same endpoint as v1: POST https://api.infusionsoft.com/token. The Edge Function requests a new token when a 401 is returned, just as in the Infusionsoft pattern.

Invoice creation in Keap v2 requires: contact_id (numeric), line_items array with { name, quantity, unit_price }, and due_date. The contact_id must be the Keap numeric contact ID, not the email address.

For tag operations in v2, apply tags via POST to /contacts/{id}/tags with body { tag_ids: [123, 456] }. Remove individual tags via DELETE /contacts/{id}/tags/{tagId} — tag removal is per-tag-ID, not bulk.

```
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

const KEAP_BASE = 'https://api.infusionsoft.com/crm/rest/v2'
const TOKEN_URL = 'https://api.infusionsoft.com/token'

async function refreshToken(): Promise<string> {
  const resp = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: Deno.env.get('KEAP_CLIENT_ID')!,
      client_secret: Deno.env.get('KEAP_CLIENT_SECRET')!,
      refresh_token: Deno.env.get('KEAP_REFRESH_TOKEN')!,
    }),
  })
  const data = await resp.json()
  if (!data.access_token) throw new Error('Keap token refresh failed: ' + JSON.stringify(data))
  return data.access_token
}

async function keapFetch(path: string, method = 'GET', body?: unknown, retry = true): Promise<Response> {
  let token = Deno.env.get('KEAP_ACCESS_TOKEN')!
  let resp = await fetch(`${KEAP_BASE}${path}`, {
    method,
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: body ? JSON.stringify(body) : undefined,
  })
  if (resp.status === 401 && retry) {
    token = await refreshToken()
    resp = await fetch(`${KEAP_BASE}${path}`, {
      method,
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
      body: body ? JSON.stringify(body) : undefined,
    })
  }
  return resp
}

serve(async (req) => {
  if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })

  try {
    const { action, params = {} } = await req.json()
    let resp: Response

    switch (action) {
      case 'find_or_create_contact': {
        const search = await keapFetch(`/contacts?filter=email==${encodeURIComponent(params.email)}`)
        const searchData = await search.json()
        if (searchData.contacts?.length > 0) {
          return new Response(JSON.stringify({ contactId: searchData.contacts[0].id, created: false }), {
            headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          })
        }
        resp = await keapFetch('/contacts', 'POST', {
          given_name: params.firstName,
          family_name: params.lastName,
          email_addresses: [{ email: params.email, field: 'EMAIL1' }],
          phone_numbers: params.phone ? [{ number: params.phone, field: 'PHONE1' }] : [],
          tag_ids: params.tagIds || [],
        })
        const created = await resp.json()
        return new Response(JSON.stringify({ contactId: created.id, created: true }), {
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
        })
      }

      case 'apply_tags':
        resp = await keapFetch(`/contacts/${params.contactId}/tags`, 'POST', { tag_ids: params.tagIds })
        break

      case 'remove_tag':
        resp = await keapFetch(`/contacts/${params.contactId}/tags/${params.tagId}`, 'DELETE')
        break

      case 'list_contacts':
        resp = await keapFetch(`/contacts?page_size=${params.pageSize || 25}&page_token=${params.pageToken || ''}`)
        break

      case 'create_invoice':
        resp = await keapFetch('/invoices', 'POST', {
          contact_id: params.contactId,
          line_items: params.lineItems,
          due_date: params.dueDate,
          notes: params.notes || '',
        })
        break

      case 'list_invoices':
        resp = await keapFetch(`/invoices?contact_id=${params.contactId || ''}&page_size=${params.pageSize || 25}`)
        break

      case 'create_opportunity':
        resp = await keapFetch('/opportunities', 'POST', {
          contact: { id: params.contactId },
          opportunity_title: params.title,
          projected_revenue_low: params.valueLow,
          projected_revenue_high: params.valueHigh,
          stage: { id: params.stageId },
        })
        break

      default:
        return new Response(JSON.stringify({ error: 'Unknown action' }), {
          status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
        })
    }

    const data = await resp.json()
    return new Response(JSON.stringify(data), {
      status: resp.ok ? 200 : resp.status,
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    })
  } catch (error) {
    return new Response(JSON.stringify({ error: error.message }), {
      status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    })
  }
})
```

**Expected result:** The keap-v2 Edge Function deploys in Cloud → Edge Functions. Test with action 'list_contacts' — Keap contacts should appear in the response with numeric IDs and the v2 field structure.

### 4. Build the Keap CRM and invoicing dashboard in Lovable

With the Edge Function deployed, use Lovable's chat to build the management portal that combines Keap's CRM and invoicing data. The unique value of Keap integration is having contacts, deals, and invoices in a single data model — your Lovable dashboard should reflect this unified view.

For a client detail page, fetch the contact record, their associated opportunities, and their invoice history in parallel (three separate Edge Function calls using Promise.all in your React component). Display as a tabbed layout: Overview (contact info + active tags), Pipeline (opportunities by stage), and Invoices (invoice list with status).

For invoice creation, build a form with line items (description, quantity, unit price) that calls the create_invoice action. The form should pre-populate the contact_id from the current client context. After creation, refresh the invoice list to show the new invoice with 'Draft' status.

For tag management, build a tag selector that calls list_tags (add this action to your Edge Function using GET /tags), shows current contact tags, and provides add/remove buttons. Applying or removing tags immediately triggers any Keap automation sequences configured for those tags.

For automation testing, apply a tag to a test contact and verify the associated Keap automation sequence fires. This confirms the API integration is working correctly end-to-end — not just that the API call returns 200, but that the automation actually runs in Keap.

RapidDev's team can help design complex Keap integrations that combine contact management, invoicing workflows, and automation triggering for complete business process automation built on your Lovable app.

**Expected result:** The client detail page renders contact information, opportunities, and invoices from Keap. Invoice creation works and the new invoice appears in Keap. Tag application triggers the configured automation sequences in Keap.

## Best practices

- Store the Keap contact ID in your Supabase users table after the first find_or_create_contact call — this enables all subsequent tag and invoice operations to use direct contact IDs rather than email searches.
- Implement automatic token refresh in your Edge Function — Keap access tokens expire after 24 hours and the integration must handle this transparently.
- Build a tag ID lookup map at app initialization by calling list_tags once and caching the result — this enables displaying tag names instead of numeric IDs throughout your UI.
- Keep Keap refresh tokens active by ensuring your integration makes at least one API call every 60 days — a weekly health check call prevents the 90-day expiry.
- Use the Keap API v2 over v1 for new integrations — v2 has better filtering, cursor-based pagination, and cleaner response structures that make Edge Function code simpler.
- Implement atomic tag swaps (apply new tag + remove old tag) for state transitions like trial-to-customer — this ensures automation sequences transition correctly without overlapping.
- Test automation sequences end-to-end by applying trigger tags to test contacts and verifying the sequences actually fire in Keap — 200 OK from the API does not confirm that automations are configured correctly.

## Use cases

### Client management portal showing contacts, pipeline, and invoices

Build an internal Lovable portal where your team manages Keap contacts, views the sales pipeline with deal stages, and creates invoices for won deals — all without switching between Keap and other tools. The portal shows a unified client view: contact details, their deal history, open invoices with payment status, and any active automation tags. Changes sync to Keap in real time.

Prompt example:

```
Create Edge Functions to fetch Keap contacts, their linked opportunities, and open invoices by contact ID. Build a client detail page in Lovable showing contact information, deal history (with stage and value), and invoice list (with status and amount). Add a Create Invoice button that calls a Keap create-invoice Edge Function with line items. Show invoice status: Draft, Sent, Paid, Overdue.
```

### Automated invoice creation on subscription or project completion

When a client completes a project milestone or a subscription renews in your Lovable app, automatically create and send a Keap invoice for the work. The Edge Function maps the billing event to Keap invoice line items, associates the invoice with the contact's record, and optionally triggers an email notification. This eliminates manual invoice creation for recurring billing scenarios.

Prompt example:

```
Create an Edge Function called keap-create-invoice that accepts { contactId, lineItems: [{ name, quantity, price }], dueDate, notes } and creates a Keap invoice using KEAP_ACCESS_TOKEN from secrets. Set the invoice status to 'draft', associate with the contact, and return the invoice ID and URL. Optionally trigger sending the invoice if params.sendNow is true.
```

### Contact tagging and automation enrollment from app events

Apply Keap tags to contacts based on behavioral events in your Lovable app — user upgraded their plan, completed onboarding, requested a callback. Tag application triggers Keap's automation sequences: email nurture flows, task creation for the sales team, and text message campaigns. This creates a live feedback loop between your app's behavioral data and Keap's marketing automation engine.

Prompt example:

```
Create an Edge Function called keap-apply-tag that accepts { contactId, tagIds } and applies the given tag IDs to the Keap contact using KEAP_ACCESS_TOKEN from secrets. Return the updated contact. Call this when a user completes onboarding (tagId: KEAP_ONBOARDING_COMPLETE_TAG), upgrades their plan (tagId: KEAP_UPGRADE_TAG), or requests a demo (tagId: KEAP_DEMO_REQUEST_TAG).
```

## Troubleshooting

### Keap API v2 returns 401 Unauthorized after 24 hours

Cause: Keap access tokens expire after 24 hours. Without token refresh logic, all API calls fail after the initial token expires.

Solution: Verify the token refresh logic in your Edge Function is implemented as shown in the code above. On a 401 response, the refreshToken() function is called, which requests a new access_token from the token endpoint using the stored refresh token and client credentials. If the refresh itself fails (e.g., the refresh token has expired after 90 days), you need to re-authorize via the OAuth2 flow.

### Contact email search returns empty array even though the contact exists in Keap

Cause: The Keap API v2 filter syntax uses == (double equals) for equality, not = or :equals as seen in other APIs.

Solution: Verify the filter parameter uses the correct syntax: filter=email==user@example.com. Also ensure the email is URL-encoded when passed as a query parameter. Note that Keap may take a few seconds to index newly created contacts for email search.

```
// Correct Keap v2 filter syntax:
const url = `/contacts?filter=email==${encodeURIComponent(email)}`
```

### Invoice creation returns 400 with contact_id validation error

Cause: The contact_id must be the numeric Keap contact ID, not the contact's email address. Passing email as contact_id returns a validation error.

Solution: Always resolve the contact's Keap numeric ID before creating invoices. Use find_or_create_contact to get the contactId, then pass that numeric ID to create_invoice. Store the Keap contact ID in your Supabase users table to avoid repeated lookups.

### Keap refresh token stops working after 90 days

Cause: Keap refresh tokens expire after 90 days of non-use. If your integration is inactive for 90+ days, the refresh token is invalidated and cannot generate new access tokens.

Solution: Re-authorize via the OAuth2 authorization code flow to obtain a new set of tokens. To prevent future expiry, ensure your integration makes at least one API call every 60 days. For production systems, set up a scheduled Supabase Edge Function that runs weekly and makes a simple list_contacts call — this keeps the refresh token active without requiring user action.

## Frequently asked questions

### What is the difference between Keap and Infusionsoft by Keap?

Infusionsoft (officially Keap Max Classic) is the legacy product with the original Infusionsoft feature set — complex campaign builder, tag-based automation engine, e-commerce tools. Modern Keap (Keap Pro and Keap Max) is a redesigned, simpler product built for entrepreneurs and small businesses. They share the same company (Keap/Infusionsoft) and the same OAuth2 Developer portal, but have different UIs and different API structures. Keap REST API v2 (this guide) covers the modern Keap product.

### Does Keap have a free plan for testing integrations?

Keap does not have a permanent free plan. A 14-day free trial is available at keap.com. The minimum paid tier is Keap Pro at $159/month for 1 user and 500 contacts. For testing Lovable integrations, the 14-day trial provides full API access. You can also use a development Keap account if you have separate production and development environments.

### Can I use the same OAuth2 application for both Keap and Infusionsoft?

Yes — one Developer portal application can be authorized against both Keap and Infusionsoft accounts. The same Client ID and Client Secret work for both products since they share the same OAuth2 authorization server (accounts.infusionsoft.com). You will have separate access and refresh tokens for each account. Switch between the Keap API v2 base URL (/crm/rest/v2/) and the Infusionsoft v1 URL (/crm/rest/v1/) based on which account's data you are accessing.

### How do I handle Keap tag automation not firing after applying tags via API?

Tag automation in Keap fires based on campaign sequences configured in the Keap Campaign Builder. Applying a tag via the API is equivalent to manually applying the tag in the UI — any campaign sequences configured to start when that tag is applied will fire. If automations are not firing, verify in Keap's Campaign Builder that the campaign is Active (published), the tag trigger matches the tag ID being applied, and the contact meets any other campaign entry criteria.

---

Source: https://www.rapidevelopers.com/lovable-integration/keap
© RapidDev — https://www.rapidevelopers.com/lovable-integration/keap
