To integrate Replit with Keap, create an API application in the Keap developer console to get OAuth 2.0 credentials or a personal access token, store them in Replit Secrets (lock icon π), and call the Keap REST API from your Python or Node.js server to manage contacts, orders, and marketing automation sequences. Autoscale deployment works well for event-driven CRM updates.
Why Connect Replit to Keap?
Keap is the CRM of choice for solopreneurs and small business owners who want automated follow-up sequences without a large marketing team. The combination of CRM contacts, tag-based automations, e-commerce order tracking, and appointment scheduling in a single platform makes Keap uniquely powerful for service businesses. Connecting your Replit app to Keap lets you automate the data flow that Keap's native integrations do not cover β adding contacts from custom forms, triggering automation sequences based on product events, and syncing order data from external payment systems.
The most powerful feature of the Keap API is the tag and automation system. In Keap, applying a tag to a contact immediately triggers any automation sequence configured to start on that tag. This means your Replit app can trigger complex multi-step follow-up sequences simply by applying the right tag β no need to call campaign-specific API endpoints.
Replit's Secrets system (lock icon π in the sidebar) is where you store Keap credentials. For simple personal automations, Keap's Personal Access Tokens are the easiest approach β a single long-lived token with no OAuth flow required. For applications serving multiple users or businesses (common in agency scenarios), full OAuth 2.0 is required. Never include Keap credentials in client-side JavaScript, as the API key grants full access to your CRM including all contact and order data.
Integration method
You connect Replit to Keap by creating an application in the Keap developer console to get OAuth 2.0 credentials, storing client credentials and access tokens in Replit Secrets, and calling the Keap REST API from server-side Python or Node.js code. For development and personal automations, Keap also offers personal access tokens (PATs) that skip the OAuth flow. The API base URL is https://api.infusionsoft.com/crm/rest/v1.
Prerequisites
- A Replit account with a Python or Node.js project created
- A Keap account (Lite, Pro, or Max tier β all include API access)
- Access to the Keap developer console at developer.keap.com
- Basic familiarity with REST APIs and OAuth 2.0
- Python 3.10+ or Node.js 18+ (both available on Replit by default)
Step-by-step guide
Obtain Keap API Credentials
Obtain Keap API Credentials
For development and personal automation workflows, the easiest approach is a Keap Personal Access Token (PAT). Visit developer.keap.com, sign in with your Keap account, and navigate to 'Personal Access Tokens'. Click 'New Token', give it a descriptive name like 'replit-integration', and copy the generated token. PATs are long-lived and work immediately without any OAuth flow. For production applications that serve multiple Keap accounts (such as a SaaS app), you need a full OAuth 2.0 application. At developer.keap.com, click 'Create New App', fill in the app name and description, and add a redirect URI for your deployed Replit URL (e.g., https://your-app.replit.app/oauth/callback). The portal provides a Client ID and Client Secret. Open your Replit project and click the lock icon π in the left sidebar. For PAT-based auth, add one secret: KEAP_API_KEY with your personal access token. For OAuth, add KEAP_CLIENT_ID, KEAP_CLIENT_SECRET, and KEAP_REFRESH_TOKEN (obtained after completing the OAuth flow once). The Keap API base URL is https://api.infusionsoft.com/crm/rest/v1 and all endpoints use Bearer token authentication.
Pro tip: Personal Access Tokens are perfect for server-to-server integrations where only your Keap account is involved. Use OAuth 2.0 only if you are building a multi-tenant application that needs to access different Keap accounts on behalf of multiple users.
Expected result: Your Keap API credentials are stored in Replit Secrets and accessible as environment variables. The KEAP_API_KEY (or OAuth tokens) appear in the Secrets pane.
Manage Contacts and Tags in Python
Manage Contacts and Tags in Python
The Keap REST API base URL is https://api.infusionsoft.com/crm/rest/v1. Authentication uses a Bearer token β whether that is a PAT or an OAuth access token, the header format is identical: Authorization: Bearer {token}. The tag system is the most powerful automation trigger in Keap. When you apply a tag to a contact via the API (POST /contacts/{id}/tags), any automation sequence configured to start on that tag begins immediately. This makes tag application the primary integration pattern: your Replit app applies the right tag at the right moment, and Keap handles the entire downstream follow-up sequence. The Python code below covers the core operations: finding a contact by email, creating a new contact, applying and removing tags, and creating a note on a contact record. The upsert pattern β search first, update if found, create if not β prevents duplicate contact records.
1import os2import requests3from typing import Optional45API_KEY = os.environ["KEAP_API_KEY"]6BASE_URL = "https://api.infusionsoft.com/crm/rest/v1"78HEADERS = {9 "Authorization": f"Bearer {API_KEY}",10 "Content-Type": "application/json"11}1213def find_contact_by_email(email: str) -> Optional[dict]:14 """Search for an existing contact by email address."""15 params = {"email": email, "limit": 1}16 response = requests.get(f"{BASE_URL}/contacts", params=params, headers=HEADERS)17 response.raise_for_status()18 contacts = response.json().get("contacts", [])19 return contacts[0] if contacts else None2021def create_contact(email: str, first_name: str, last_name: str = "",22 phone: str = "") -> dict:23 """Create a new contact in Keap."""24 payload = {25 "email_addresses": [{"email": email, "field": "EMAIL1"}],26 "given_name": first_name,27 "family_name": last_name,28 "duplicate_option": "Email" # Merge if email exists29 }30 if phone:31 payload["phone_numbers"] = [{"number": phone, "field": "PHONE1"}]3233 response = requests.post(f"{BASE_URL}/contacts", json=payload, headers=HEADERS)34 response.raise_for_status()35 return response.json()3637def apply_tag(contact_id: int, tag_id: int) -> dict:38 """Apply a tag to a contact (triggers any automation sequences on that tag)."""39 payload = {"tagIds": [tag_id]}40 response = requests.post(41 f"{BASE_URL}/contacts/{contact_id}/tags",42 json=payload,43 headers=HEADERS44 )45 response.raise_for_status()46 return response.json()4748def remove_tag(contact_id: int, tag_id: int) -> None:49 """Remove a tag from a contact."""50 response = requests.delete(51 f"{BASE_URL}/contacts/{contact_id}/tags/{tag_id}",52 headers=HEADERS53 )54 response.raise_for_status()5556def get_tags() -> list:57 """Get all tags in the Keap account."""58 response = requests.get(f"{BASE_URL}/tags", headers=HEADERS)59 response.raise_for_status()60 return response.json().get("tags", [])6162def create_note(contact_id: int, title: str, body: str,63 note_type: str = "Other") -> dict:64 """Add a note to a contact record."""65 payload = {66 "contact_id": contact_id,67 "title": title,68 "body": body,69 "type": note_type # Appointment, Call, Email, Fax, Letter, Meeting, Other70 }71 response = requests.post(f"{BASE_URL}/notes", json=payload, headers=HEADERS)72 response.raise_for_status()73 return response.json()7475# Example usage76if __name__ == "__main__":77 # Get all tags to find the right ID78 tags = get_tags()79 print("Available tags:")80 for tag in tags[:10]:81 print(f" {tag['id']}: {tag['name']}")8283 # Create or find a contact84 contact = find_contact_by_email("test@example.com")85 if not contact:86 contact = create_contact("test@example.com", "Test", "User")87 print(f"Contact created: ID {contact['id']}")88 else:89 print(f"Contact found: ID {contact['id']}")9091 # Apply a tag to trigger automation (replace 123 with real tag ID)92 if tags:93 apply_tag(contact['id'], tags[0]['id'])94 print(f"Tag applied: {tags[0]['name']}")Pro tip: Call GET /tags at the start of your integration to print all tag IDs and names. Store the IDs you need in Replit Secrets (e.g., KEAP_ONBOARDING_TAG_ID) rather than hardcoding numbers, so they are easy to update without changing code.
Expected result: The script prints your Keap tags, creates or finds a test contact, and applies a tag. The contact appears in Keap with the tag applied and any associated automation sequence begins.
Build a Node.js Server with Order Management
Build a Node.js Server with Order Management
The Node.js integration uses the same Bearer token authentication as Python. The Express server below exposes a contact enrollment endpoint and an order creation endpoint, covering the two most common Keap integration patterns: inbound lead capture and order syncing from external payment processors. Keap's order API lets you create orders linked to contacts and associate them with product records. This is useful when you process payments outside of Keap (e.g., via Stripe) but want the purchase data visible in the CRM for post-purchase automation sequences. Creating an order in Keap automatically marks the contact as a 'customer' and can trigger purchase-specific automations. Install dependencies with 'npm install express axios' in the Replit shell. The server also implements a basic search-before-create pattern in the /enroll endpoint to prevent duplicate contact records.
1const express = require('express');2const axios = require('axios');34const app = express();5app.use(express.json());67const API_KEY = process.env.KEAP_API_KEY;8const ONBOARDING_TAG_ID = parseInt(process.env.KEAP_ONBOARDING_TAG_ID || '0');9const BASE_URL = 'https://api.infusionsoft.com/crm/rest/v1';1011const keap = axios.create({12 baseURL: BASE_URL,13 headers: {14 'Authorization': `Bearer ${API_KEY}`,15 'Content-Type': 'application/json'16 }17});1819async function findOrCreateContact(email, firstName, lastName) {20 // Search for existing contact21 const searchResp = await keap.get('/contacts', { params: { email, limit: 1 } });22 const contacts = searchResp.data.contacts || [];23 if (contacts.length > 0) return contacts[0];2425 // Create new contact26 const createResp = await keap.post('/contacts', {27 email_addresses: [{ email, field: 'EMAIL1' }],28 given_name: firstName,29 family_name: lastName || '',30 duplicate_option: 'Email'31 });32 return createResp.data;33}3435// Enroll a new lead and apply onboarding tag36app.post('/enroll', async (req, res) => {37 const { email, firstName, lastName, source } = req.body;38 if (!email || !firstName) {39 return res.status(400).json({ error: 'email and firstName are required' });40 }4142 try {43 const contact = await findOrCreateContact(email, firstName, lastName);44 const contactId = contact.id;4546 // Apply onboarding tag to trigger automation sequence47 if (ONBOARDING_TAG_ID) {48 await keap.post(`/contacts/${contactId}/tags`, {49 tagIds: [ONBOARDING_TAG_ID]50 });51 }5253 // Add a source note54 if (source) {55 await keap.post('/notes', {56 contact_id: contactId,57 title: 'Lead Source',58 body: `Contact enrolled from: ${source}`,59 type: 'Other'60 });61 }6263 res.json({ success: true, contactId });64 } catch (err) {65 console.error('Enroll error:', err.response?.data || err.message);66 res.status(500).json({ error: err.message });67 }68});6970// Create an order from an external payment71app.post('/orders', async (req, res) => {72 const { email, productName, amount, orderId } = req.body;73 if (!email || !amount) {74 return res.status(400).json({ error: 'email and amount are required' });75 }7677 try {78 // Find the contact79 const searchResp = await keap.get('/contacts', { params: { email, limit: 1 } });80 const contacts = searchResp.data.contacts || [];81 if (!contacts.length) {82 return res.status(404).json({ error: 'Contact not found in Keap' });83 }8485 const contactId = contacts[0].id;8687 // Create the order88 const orderResp = await keap.post('/orders', {89 contact_id: contactId,90 order_date: new Date().toISOString(),91 order_items: [{92 description: productName || 'Product Purchase',93 price: amount,94 quantity: 195 }],96 title: orderId ? `Order ${orderId}` : 'External Order',97 source_type: 'ONLINE'98 });99100 res.json({ success: true, orderId: orderResp.data.id });101 } catch (err) {102 console.error('Order error:', err.response?.data || err.message);103 res.status(500).json({ error: err.message });104 }105});106107app.get('/health', (req, res) => res.json({ status: 'ok' }));108109app.listen(3000, '0.0.0.0', () => {110 console.log('Keap integration server running on port 3000');111});Pro tip: Test the /enroll endpoint with a real email address from your Keap account to verify that the automation sequence fires after the tag is applied. Check the contact's automation history in Keap to confirm the sequence started.
Expected result: The server starts and POST /enroll creates a contact in Keap, applies the onboarding tag, and returns the contactId. The automation sequence tied to the tag begins running in Keap.
Deploy and Configure Webhook Triggers
Deploy and Configure Webhook Triggers
Keap supports outbound webhooks (called 'Hook' subscriptions in the API) that POST to your Replit server when contacts or orders change. This enables bidirectional integration β your app can react to Keap events like tag removal (unsubscribing from a sequence) or order creation (fulfilling a purchase). To create a webhook subscription programmatically, call POST /hooks with the event_key (e.g., 'contact.add', 'order.add', 'contact.delete') and your webhook URL. Alternatively, set up webhooks in the Keap UI under Settings > API > Webhooks. Keap sends a JSON POST to your endpoint with the hook event and the affected record's ID. Deploy your Replit app before registering webhook URLs. Click 'Deploy' in the Replit editor and choose Autoscale for event-driven CRM workflows. Copy the stable deployment URL (e.g., https://your-app.replit.app) and use it as the webhook target. Keap webhooks include a X-Hook-Secret header on the initial verification request β respond with the secret value to confirm the endpoint.
1from flask import Flask, request, jsonify2import os3import hmac4import hashlib56app = Flask(__name__)7HOOK_SECRET = os.environ.get("KEAP_HOOK_SECRET", "")89@app.route('/keap/webhook', methods=['POST'])10def keap_webhook():11 # Verify hook secret on first delivery12 hook_secret = request.headers.get('X-Hook-Secret')13 if hook_secret:14 # Keap sends X-Hook-Secret on first delivery for verification15 return hook_secret, 200, {'X-Hook-Secret': hook_secret}1617 data = request.get_json(force=True)18 if not data:19 return jsonify({'error': 'No data'}), 4002021 event_key = data.get('event_key', 'unknown')22 object_keys = data.get('object_keys', [])2324 print(f"Keap webhook: {event_key}, IDs: {object_keys}")2526 if event_key == 'contact.add':27 for contact_id in object_keys:28 print(f"New contact added: {contact_id}")29 # Fetch full contact data if needed3031 elif event_key == 'order.add':32 for order_id in object_keys:33 print(f"New order created: {order_id}")34 # Trigger fulfillment workflow3536 return jsonify({'status': 'received'}), 2003738@app.route('/health', methods=['GET'])39def health():40 return jsonify({'status': 'ok'}), 2004142if __name__ == '__main__':43 app.run(host='0.0.0.0', port=3000)Pro tip: Keap webhook payloads only include the IDs of affected records, not the full record data. Make a follow-up GET request to /contacts/{id} or /orders/{id} to retrieve the full details you need.
Expected result: Your deployed Replit server receives Keap webhook events and logs the event type and record IDs. The /health endpoint returns 200.
Common use cases
Contact Creation and Sequence Enrollment from Web Forms
When a visitor fills out a lead generation form on your Replit-hosted website, automatically create or update the Keap contact record, apply the appropriate intake tag, and trigger a pre-built follow-up sequence. The automated sequence sends personalized emails and tasks to the sales team without any manual intervention.
Build a Flask form handler that creates a Keap contact from form submission data stored in Replit Secrets, applies a 'webinar-signup' tag using the tag ID, and returns a success message to the user.
Copy this prompt to try it in Replit
E-commerce Order Sync from External Store
When a purchase is completed in your Replit e-commerce app or an external payment processor, create a Keap order linked to the contact's account and apply a 'customer' tag. Keap's post-purchase automation sequences automatically send receipts, onboarding emails, and upsell sequences based on the product purchased.
Write a Node.js webhook handler that receives Stripe payment confirmation events and creates a corresponding Keap order with the product and total amount, then applies a 'paying-customer' tag to the contact.
Copy this prompt to try it in Replit
Behavioral Trigger Automations
Your Replit app monitors user behavior β inactivity periods, feature adoption milestones, or subscription renewals β and applies specific Keap tags to trigger the matching automation sequence. This creates a closed loop between product usage data and marketing follow-up without requiring Keap to have direct access to your application database.
Create a Python script that queries your database for users who haven't logged in for 7 days, finds their Keap contact by email, and applies a 're-engagement' tag to trigger the win-back automation sequence.
Copy this prompt to try it in Replit
Troubleshooting
API returns 401 Unauthorized with 'Invalid or missing auth token'
Cause: The Bearer token in the Authorization header is invalid, expired, or missing. Personal access tokens do not expire but OAuth access tokens expire after one hour.
Solution: Verify the KEAP_API_KEY Secret in Replit Secrets contains the correct personal access token with no extra whitespace. If using OAuth, implement token refresh using the refresh token. Test the token by calling GET /profile (the simplest endpoint) to confirm it works.
1# Python: test your token2response = requests.get(3 "https://api.infusionsoft.com/crm/rest/v1/profile",4 headers={"Authorization": f"Bearer {os.environ['KEAP_API_KEY']}"}5)6print(response.status_code, response.json())Tag application returns 404 β tag not found
Cause: The tag ID does not exist in the Keap account. Tag IDs are account-specific integers that cannot be transferred between accounts.
Solution: Call GET /tags to list all available tags with their IDs. Update your KEAP_ONBOARDING_TAG_ID or equivalent Replit Secret with the correct integer ID. Create any missing tags via POST /tags before applying them.
1# List all tags2response = requests.get(3 f"{BASE_URL}/tags",4 headers=HEADERS5)6for tag in response.json().get('tags', []):7 print(f"Tag {tag['id']}: {tag['name']}")Contact creation returns 400 with 'duplicate contact' error
Cause: A contact with the same email already exists in Keap. The API rejects duplicate emails by default unless duplicate_option is set.
Solution: Include 'duplicate_option': 'Email' in the contact creation payload to merge with an existing record. Alternatively, search for the contact first and update the existing record if found.
1# Python: use duplicate_option to merge2payload = {3 "email_addresses": [{"email": email, "field": "EMAIL1"}],4 "given_name": first_name,5 "duplicate_option": "Email" # Merge on email match6}Webhook events are not arriving at the Replit server
Cause: The webhook URL points to a development Replit session that goes offline when the IDE is closed, or the endpoint did not respond to the initial verification request with the X-Hook-Secret header.
Solution: Deploy your Replit app first to get a stable URL. Ensure your webhook handler returns the X-Hook-Secret header value in its response to the first POST request from Keap, which serves as endpoint verification. After deploying, re-register the webhook URL in Keap settings.
Best practices
- Store KEAP_API_KEY and tag IDs in Replit Secrets (lock icon π) β never hardcode API tokens or tag IDs directly in source code.
- Use Personal Access Tokens for single-account server-to-server integrations to avoid the OAuth flow complexity β PATs are simpler to manage and do not expire.
- Always include 'duplicate_option': 'Email' when creating contacts to prevent duplicate records when the same person submits a form multiple times.
- Retrieve tag IDs dynamically via GET /tags rather than hardcoding them, and store the IDs you need as named Secrets for easy updating.
- Use tag application as your primary automation trigger β applying a tag is simpler and more reliable than calling campaign-specific API endpoints.
- Deploy your Replit app before registering webhook URLs with Keap β stable deployment URLs prevent dead webhook registrations.
- Handle Keap's webhook payload format correctly: the payload only includes record IDs, so you must make a follow-up GET request to retrieve full record data.
- Use Autoscale deployment for CRM update integrations and Reserved VM if you need guaranteed webhook delivery without cold-start delays.
Alternatives
HubSpot scales better for growing teams and has more extensive marketing automation features, making it the better choice if you expect to outgrow Keap's small-business focus.
Freshsales is a strong alternative CRM with simpler API key authentication (no OAuth required) and is part of a broader Freshworks ecosystem including support and chat tools.
Autopilot focuses specifically on visual journey automation without the CRM and e-commerce complexity, making it a lighter-weight option for teams that only need marketing sequences.
Frequently asked questions
How do I store my Keap API key in Replit?
Click the lock icon π in the left sidebar of your Replit project to open the Secrets pane. Add KEAP_API_KEY with your Keap Personal Access Token (from developer.keap.com > Personal Access Tokens). Access it in Python with os.environ['KEAP_API_KEY'] or in Node.js with process.env.KEAP_API_KEY.
What is the difference between a Keap Personal Access Token and OAuth?
A Personal Access Token (PAT) is a long-lived token that gives access to a single Keap account with no expiration and no OAuth flow required. OAuth 2.0 is needed when your app must access multiple Keap accounts (e.g., a SaaS app serving multiple businesses). For server-to-server integrations with your own Keap account, PATs are the simpler and recommended approach.
Does Replit work with Keap on the free plan?
Yes. The Keap REST API is available on all Keap plans including Lite. Replit's free tier supports outbound API calls to Keap without restrictions. You will need Replit's paid plan for always-on deployments required for webhook reception, since free Replit projects sleep after inactivity.
How do I trigger a Keap automation sequence from Replit?
Apply the tag associated with the automation sequence to the contact via POST /contacts/{id}/tags with the tag ID in the tagIds array. Any Keap automation configured to start when that tag is applied will begin immediately after the API call. This is the primary automation trigger pattern and is simpler than calling campaign-specific endpoints.
Can I create orders in Keap from external payment processors like Stripe?
Yes. Use POST /orders to create an order linked to a contact ID. Include the product name, price, and quantity in the order_items array. First find the contact by email using GET /contacts?email={email}, then use the contact's ID in the order creation payload. This keeps your purchase history visible in Keap for post-purchase automations.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation