To integrate Replit with Constant Contact, create an API application in the Constant Contact developer portal to get a client ID and secret, store them in Replit Secrets (lock icon 🔒), implement the OAuth 2.0 flow, and call the Constant Contact v3 API from your Python or Node.js server to manage contacts and email campaigns.
Why Connect Replit to Constant Contact?
Constant Contact is one of the longest-running email marketing platforms, particularly popular with small businesses, nonprofits, and local organizations. Its v3 API provides programmatic access to the same features available in the UI: contact management, list segmentation, email campaign creation, and event registration. Connecting your Replit app to Constant Contact lets you automate the tedious parts — adding new subscribers, syncing customer data, and scheduling campaigns — without manual CSV exports or copy-paste workflows.
The primary integration patterns are syncing new customers or users from your app directly into Constant Contact lists, updating contact fields when user data changes in your database, and triggering campaigns in response to product events. Because Constant Contact is aimed at non-technical marketers, the API fills the gap between your dynamic application data and the static contact lists that marketers manage in the UI.
Replit's Secrets system (lock icon 🔒 in the sidebar) is essential for this integration because the OAuth client secret must never appear in source code. Constant Contact uses OAuth 2.0, which means you also need to handle access token refresh — tokens expire after an hour. Your Replit server manages this token lifecycle, storing the refresh token in Secrets and using it to obtain a fresh access token before each API call.
Integration method
You connect Replit to Constant Contact by registering an API application in the Constant Contact developer portal to obtain OAuth 2.0 credentials, storing the client ID and secret in Replit Secrets, and calling the Constant Contact v3 API from your server-side Python or Node.js code. OAuth access tokens must be refreshed periodically, so your server handles token management and persists refresh tokens between requests.
Prerequisites
- A Replit account with a Python or Node.js project created
- A Constant Contact account (free trial or paid)
- Access to the Constant Contact developer portal at developer.constantcontact.com
- Basic familiarity with OAuth 2.0 authorization flows
- Python 3.10+ or Node.js 18+ (both available on Replit by default)
Step-by-step guide
Create an API Application and Get OAuth Credentials
Create an API Application and Get OAuth Credentials
Visit the Constant Contact developer portal at developer.constantcontact.com and sign in with your Constant Contact account. Navigate to 'My Applications' and click 'New Application'. Give your app a name like 'Replit Integration' and fill in the required description. For the redirect URI, enter your deployed Replit app URL plus a callback path (e.g., https://your-app.replit.app/oauth/callback). If you do not have a deployed URL yet, use a placeholder like https://localhost:3000/oauth/callback and update it after deployment. After creating the application, Constant Contact provides a Client ID (also called the API Key) and a Client Secret. Copy both values immediately — the client secret may not be visible again without regenerating it. Store both in Replit Secrets: CONSTANT_CONTACT_CLIENT_ID and CONSTANT_CONTACT_CLIENT_SECRET. You will also need to complete the OAuth flow once to get an initial access token and refresh token. Constant Contact uses the authorization code flow: your app redirects users to the Constant Contact authorization page, users approve access, and Constant Contact redirects back to your callback URL with a code that you exchange for tokens. For server-to-server integrations, you can perform this flow once manually and store the long-lived refresh token in Replit Secrets as CONSTANT_CONTACT_REFRESH_TOKEN.
Pro tip: Constant Contact's OAuth access tokens expire after one hour. Always use the refresh token to get a new access token before making API calls, rather than storing the access token directly in Secrets.
Expected result: You have a Client ID, Client Secret, and after completing the OAuth flow, a refresh token — all stored in Replit Secrets.
Implement OAuth Token Management in Python
Implement OAuth Token Management in Python
The most critical part of the Constant Contact integration is handling OAuth token refresh correctly. Access tokens expire every 3600 seconds, so your code must check token expiry before each API call and refresh using the stored refresh token when needed. If the refresh fails, the integration stops working — build monitoring around it. The Python module below handles the full token lifecycle: it reads the refresh token from Replit Secrets (os.environ), exchanges it for a fresh access token, and caches the token in memory for the duration of the server process. A SQLite file or Replit's own DB can be used for persistence between server restarts, but for simplicity this example uses module-level caching with an expiry timestamp. The Constant Contact v3 API base URL is https://api.cc.email/v3. The token endpoint is https://authz.constantcontact.com/oauth2/default/v1/token. All API requests use the access token as a Bearer token in the Authorization header.
1import os2import time3import requests4from base64 import b64encode5from typing import Optional67CLIENT_ID = os.environ["CONSTANT_CONTACT_CLIENT_ID"]8CLIENT_SECRET = os.environ["CONSTANT_CONTACT_CLIENT_SECRET"]9REFRESH_TOKEN = os.environ["CONSTANT_CONTACT_REFRESH_TOKEN"]10BASE_URL = "https://api.cc.email/v3"11TOKEN_URL = "https://authz.constantcontact.com/oauth2/default/v1/token"1213# In-memory token cache (use a database for multi-process deployments)14_token_cache = {"access_token": None, "expires_at": 0}1516def get_access_token() -> str:17 """Get a valid access token, refreshing if expired."""18 if time.time() < _token_cache["expires_at"] - 60:19 return _token_cache["access_token"]2021 # Encode credentials for Basic Auth22 credentials = b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()23 headers = {24 "Authorization": f"Basic {credentials}",25 "Content-Type": "application/x-www-form-urlencoded"26 }27 data = {28 "grant_type": "refresh_token",29 "refresh_token": REFRESH_TOKEN30 }31 response = requests.post(TOKEN_URL, headers=headers, data=data)32 response.raise_for_status()33 token_data = response.json()3435 _token_cache["access_token"] = token_data["access_token"]36 _token_cache["expires_at"] = time.time() + token_data.get("expires_in", 3600)37 return _token_cache["access_token"]3839def api_headers() -> dict:40 return {41 "Authorization": f"Bearer {get_access_token()}",42 "Content-Type": "application/json"43 }4445def add_contact(email: str, first_name: str = "", last_name: str = "",46 list_ids: list = None) -> dict:47 """Add or update a contact in Constant Contact."""48 contact_data = {49 "email_address": {"address": email},50 "first_name": first_name,51 "last_name": last_name,52 "create_source": "Account"53 }54 if list_ids:55 contact_data["list_memberships"] = list_ids5657 response = requests.post(58 f"{BASE_URL}/contacts",59 json=contact_data,60 headers=api_headers()61 )62 response.raise_for_status()63 return response.json()6465def get_contact_lists() -> list:66 """Retrieve all contact lists in the account."""67 response = requests.get(f"{BASE_URL}/contact_lists", headers=api_headers())68 response.raise_for_status()69 return response.json().get("lists", [])7071def add_contact_to_list(contact_id: str, list_id: str) -> None:72 """Add an existing contact to a specific list."""73 response = requests.put(74 f"{BASE_URL}/contacts/{contact_id}",75 json={"list_memberships": [list_id]},76 headers=api_headers()77 )78 response.raise_for_status()7980# Example usage81if __name__ == "__main__":82 lists = get_contact_lists()83 print("Available lists:")84 for lst in lists:85 print(f" {lst['list_id']}: {lst['name']}")8687 if lists:88 result = add_contact(89 email="test@example.com",90 first_name="Test",91 last_name="User",92 list_ids=[lists[0]["list_id"]]93 )94 print(f"Contact added: {result.get('contact_id')}")Pro tip: The Constant Contact v3 API returns a 409 Conflict if a contact with the same email already exists. Handle this by catching the 409 and using the PUT /contacts/{id} endpoint to update the existing record instead.
Expected result: Running the script prints your available Constant Contact lists and adds a test contact to the first list. The contact appears in your Constant Contact account within seconds.
Build a Node.js Integration Server
Build a Node.js Integration Server
The Node.js Express server below implements the same token management pattern as the Python version, with an in-memory token cache and automatic refresh. It exposes endpoints for subscribing contacts and listing available contact lists. Install dependencies with 'npm install express axios' in the Replit shell. The server also includes a basic OAuth callback handler (/oauth/callback) that you can use during the initial setup to capture the authorization code and exchange it for tokens. Once you have the refresh token, you can remove or disable the callback route and hardcode the refresh token in Replit Secrets for the long term. For production use, consider persisting the refresh token back to Replit Secrets (or a database) if Constant Contact issues a new refresh token during the refresh flow — some OAuth providers rotate refresh tokens on each use. Constant Contact currently does not rotate refresh tokens, but monitoring the refresh response for a new refresh_token field is good practice.
1const express = require('express');2const axios = require('axios');3const { Buffer } = require('buffer');45const app = express();6app.use(express.json());78const CLIENT_ID = process.env.CONSTANT_CONTACT_CLIENT_ID;9const CLIENT_SECRET = process.env.CONSTANT_CONTACT_CLIENT_SECRET;10const REFRESH_TOKEN = process.env.CONSTANT_CONTACT_REFRESH_TOKEN;11const BASE_URL = 'https://api.cc.email/v3';12const TOKEN_URL = 'https://authz.constantcontact.com/oauth2/default/v1/token';1314// In-memory token cache15let tokenCache = { accessToken: null, expiresAt: 0 };1617async function getAccessToken() {18 if (Date.now() / 1000 < tokenCache.expiresAt - 60) {19 return tokenCache.accessToken;20 }21 const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');22 const response = await axios.post(23 TOKEN_URL,24 new URLSearchParams({25 grant_type: 'refresh_token',26 refresh_token: REFRESH_TOKEN27 }),28 {29 headers: {30 'Authorization': `Basic ${credentials}`,31 'Content-Type': 'application/x-www-form-urlencoded'32 }33 }34 );35 tokenCache.accessToken = response.data.access_token;36 tokenCache.expiresAt = Date.now() / 1000 + (response.data.expires_in || 3600);37 return tokenCache.accessToken;38}3940async function apiHeaders() {41 const token = await getAccessToken();42 return {43 'Authorization': `Bearer ${token}`,44 'Content-Type': 'application/json'45 };46}4748// Get all contact lists49app.get('/lists', async (req, res) => {50 try {51 const headers = await apiHeaders();52 const { data } = await axios.get(`${BASE_URL}/contact_lists`, { headers });53 res.json(data.lists || []);54 } catch (err) {55 console.error('Lists error:', err.response?.data || err.message);56 res.status(500).json({ error: err.message });57 }58});5960// Subscribe a contact to a list61app.post('/subscribe', async (req, res) => {62 const { email, firstName, lastName, listId } = req.body;63 if (!email || !listId) {64 return res.status(400).json({ error: 'email and listId are required' });65 }66 try {67 const headers = await apiHeaders();68 const payload = {69 email_address: { address: email },70 first_name: firstName || '',71 last_name: lastName || '',72 create_source: 'Account',73 list_memberships: [listId]74 };75 const { data } = await axios.post(`${BASE_URL}/contacts`, payload, { headers });76 res.json({ success: true, contactId: data.contact_id });77 } catch (err) {78 if (err.response?.status === 409) {79 res.status(409).json({ error: 'Contact already exists', detail: err.response.data });80 } else {81 console.error('Subscribe error:', err.response?.data || err.message);82 res.status(500).json({ error: err.message });83 }84 }85});8687// Health check88app.get('/health', (req, res) => res.json({ status: 'ok' }));8990app.listen(3000, '0.0.0.0', () => {91 console.log('Constant Contact integration server running on port 3000');92});Pro tip: Use 'npm install express axios' in the Replit shell to install dependencies. Both packages are lightweight and work well in Replit's Node.js environment.
Expected result: The Express server starts and the GET /lists endpoint returns all contact lists from your Constant Contact account. The POST /subscribe endpoint adds a test contact to the specified list.
Deploy and Test the Integration
Deploy and Test the Integration
Before deploying, make sure all required Replit Secrets are set: CONSTANT_CONTACT_CLIENT_ID, CONSTANT_CONTACT_CLIENT_SECRET, and CONSTANT_CONTACT_REFRESH_TOKEN. If you have not yet completed the OAuth flow to obtain a refresh token, you need to do so first — run the server locally (in the Replit development environment), visit the /oauth/callback setup URL, and authorize your application. Once your Secrets are configured, click 'Deploy' in the Replit editor. For this integration, Autoscale is the recommended deployment type because email marketing operations are triggered by user actions or scheduled tasks rather than continuous background work. Autoscale scales down to zero when there is no traffic, reducing costs. After deployment, copy your stable URL from the Deployments panel (e.g., https://your-app.replit.app) and update the Redirect URI in the Constant Contact developer portal to match. This is required for future OAuth re-authorization if your refresh token expires. Test the integration by hitting the /health endpoint first, then the /lists endpoint to confirm the token refresh is working correctly.
Pro tip: Constant Contact refresh tokens are long-lived but can expire if unused for extended periods. Set up a weekly scheduled task or cron job in Replit to make a test API call and ensure the token remains active.
Expected result: Your deployed app is accessible at https://your-app.replit.app, the /health endpoint returns 200, and /lists returns your Constant Contact contact lists.
Common use cases
Automatic Contact Sync from App Registration
When users register in your Replit web application, automatically add them to a Constant Contact list with their name and any relevant tags. The server-side integration ensures the contact data is added securely without exposing the API credentials to the browser, and new contacts immediately become available for email campaigns in Constant Contact.
Build a Flask registration endpoint that creates a user record in PostgreSQL and adds them to a Constant Contact email list using CONSTANT_CONTACT_ACCESS_TOKEN from Replit Secrets, tagging them based on the registration source.
Copy this prompt to try it in Replit
Newsletter Subscription Management Portal
Build a custom subscription management page in your Replit app that allows users to opt in or out of specific Constant Contact lists. The backend adds or removes the contact from the relevant list IDs, giving users control over their email preferences without needing to log into Constant Contact's UI.
Create an Express server with GET /subscription-preferences and POST /subscription-update endpoints that read and update a user's Constant Contact list memberships using the v3 API, displaying all available lists from the account.
Copy this prompt to try it in Replit
Event-Driven Campaign Trigger
When a user completes a significant action in your app — completing a purchase, reaching a usage milestone, or requesting a quote — your Replit server adds a custom tag to their Constant Contact contact record. Constant Contact automations can then fire a targeted email campaign in response to that tag without further code changes.
Write a Python function that adds a 'requested_demo' tag to an existing Constant Contact contact record when a demo request form is submitted, using the contact's email address as the lookup key.
Copy this prompt to try it in Replit
Troubleshooting
Token refresh returns 401 or 'invalid_client' error
Cause: The CLIENT_ID or CLIENT_SECRET is incorrect, or the credentials are being passed incorrectly. Constant Contact requires client credentials to be Base64-encoded and sent as HTTP Basic Auth, not as form body parameters.
Solution: Verify that CONSTANT_CONTACT_CLIENT_ID and CONSTANT_CONTACT_CLIENT_SECRET match exactly what is shown in the developer portal. The credentials must be Base64-encoded as 'clientId:clientSecret' and passed in the Authorization: Basic header. Check that there are no extra spaces or newline characters in the secrets.
1# Python: correct Basic Auth encoding2from base64 import b64encode3credentials = b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()4headers = {"Authorization": f"Basic {credentials}"}POST /contacts returns 409 Conflict
Cause: A contact with the same email address already exists in the Constant Contact account. The POST endpoint creates new contacts only — it does not update existing ones.
Solution: Catch the 409 response and use the contact_id from the error response to call PUT /contacts/{id} to update the existing record instead. The 409 response body typically includes the existing contact's ID.
1# Python: handle existing contact2try:3 response = requests.post(f"{BASE_URL}/contacts", json=data, headers=api_headers())4 response.raise_for_status()5except requests.HTTPError as e:6 if e.response.status_code == 409:7 existing = e.response.json()8 contact_id = existing.get('contact_id')9 if contact_id:10 requests.put(f"{BASE_URL}/contacts/{contact_id}",11 json=data, headers=api_headers())Access token expires and API calls return 401 mid-session
Cause: The in-memory token cache is not persistent across server restarts. After a Replit deployment restart or cold start, the cache is empty and the code must use the refresh token to get a new access token before the first API call.
Solution: Ensure your get_access_token() function always checks the cached expiry time and calls the token endpoint when the token is missing or close to expiry. Adding a 60-second buffer (checking if expires_at - 60 > now) prevents edge cases where the token expires during an API call.
Contact list IDs are not recognized — API returns 400 Bad Request
Cause: Contact list IDs in Constant Contact v3 are UUIDs (e.g., 'd89786f4-ab11-41bb-b1d1-0a6e7a10c2b1'), not short numeric IDs. Providing a numeric ID or an incorrect UUID format causes a 400 error.
Solution: Retrieve your list IDs by calling GET /contact_lists from your Replit server first. Copy the exact UUID strings from the API response rather than from the Constant Contact UI URL, which may show a different identifier format.
Best practices
- Store OAuth credentials (client ID, client secret, refresh token) in Replit Secrets (lock icon 🔒) — never in source code or committed .env files.
- Always implement token refresh logic before API calls — Constant Contact access tokens expire after one hour and requests with expired tokens return 401.
- Cache the access token in memory with an expiry timestamp to avoid making a token refresh request before every single API call.
- Handle 409 Conflict responses from POST /contacts gracefully by falling back to a PUT update, since duplicate emails are a normal occurrence in production.
- Retrieve contact list IDs dynamically via GET /contact_lists rather than hardcoding them — list IDs are UUIDs and can change if lists are deleted and recreated.
- Deploy your Replit app before registering webhook or redirect URIs in the developer portal — use your stable replit.app deployment URL.
- Use Autoscale deployment for marketing automation workflows triggered by user actions, since it handles variable traffic without always-on resource costs.
- Monitor your token refresh flow with logging — a silently failing token refresh will stop all API calls and is easy to miss without visible errors.
Alternatives
Mailchimp has a more powerful API with better automation features and a larger free tier, making it the preferred choice for developers who need advanced segmentation and campaign triggers.
SendGrid is better for transactional email and has native Replit integration, making it easier to set up for sending individual emails like receipts and password resets rather than managing marketing lists.
Campaign Monitor has a simpler API structure than Constant Contact and is a good alternative if your primary need is sending beautifully designed campaigns to a managed subscriber list.
Frequently asked questions
How do I get a Constant Contact refresh token for use in Replit?
You must complete the OAuth 2.0 authorization code flow once. Run your Replit server, redirect your browser to the Constant Contact authorization URL with your client ID and redirect URI, approve access, and your server's callback handler receives a code. Exchange that code for tokens using the token endpoint — the response includes both an access token and a refresh token. Store the refresh token in Replit Secrets as CONSTANT_CONTACT_REFRESH_TOKEN.
Does Replit work with Constant Contact on the free tier?
Yes. Outbound API calls to Constant Contact work from Replit's free tier. The Constant Contact API itself is available on all paid Constant Contact plans. Note that you need Replit's paid plan for always-on deployments if your use case requires permanent webhook endpoints, since free Replit projects go to sleep when inactive.
How do I find my Constant Contact list IDs?
Call GET https://api.cc.email/v3/contact_lists from your Replit server with a valid access token. The response includes an array of list objects, each with a list_id field (a UUID string) and a name. Use these UUIDs — not the list names or any IDs shown in the Constant Contact UI URL — when adding contacts to lists via the API.
Why does my Constant Contact API call fail with 401 after working correctly?
Constant Contact access tokens expire after one hour. Your code must check the token expiry before each API call and use the refresh token to get a new access token when needed. Ensure your token cache includes an expiry timestamp and that the refresh logic runs before the API call, not after it fails.
Can I create and send email campaigns via the Constant Contact API from Replit?
Yes. The Constant Contact v3 API includes endpoints for creating email campaigns (POST /emails), adding content (PUT /emails/{id}/send_history), and scheduling sends. Campaign creation requires specifying the list, subject line, from name, and HTML content. Use the /emails endpoint to create the draft and the /emails/{id}/send endpoint to trigger the send.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation