To integrate Oracle Eloqua with Lovable, create a Supabase Edge Function that authenticates via OAuth2 or Basic auth. The critical first step is discovering your Eloqua API base URL by querying login.eloqua.com — each Eloqua instance has a unique Pod URL that must be discovered before any other API call can succeed. Store your Eloqua credentials in Cloud Secrets and use the Pod URL to proxy all REST API requests.
Connect Lovable to Oracle Eloqua for Enterprise Marketing Automation
Oracle Eloqua is the enterprise marketing automation platform of choice for large B2B organizations with complex multi-channel campaign requirements. It powers sophisticated lead management workflows, advanced lead scoring with custom scoring rules, multi-stage nurture programs, and deep integration with Oracle CRM, Salesforce, and Microsoft Dynamics. At scale — marketing to hundreds of thousands or millions of contacts — Eloqua's segmentation and campaign execution capabilities exceed those of smaller platforms.
The unique technical characteristic that distinguishes Eloqua from other marketing automation APIs is its Pod architecture. Unlike SaaS platforms where all customers share the same API endpoint, Eloqua deploys each customer on a dedicated Pod (a separate infrastructure cluster). Your Eloqua instance might be on Pod 1 (p01.eloqua.com), Pod 2 (p02.eloqua.com), or another pod. You cannot know which Pod your instance is on without first querying Eloqua's login discovery endpoint at login.eloqua.com — it returns your instance's Pod URL as part of the authentication response.
This two-step flow is mandatory for all Eloqua API integrations: discover the Pod URL first, then use it as the base for all subsequent API calls. Your Supabase Edge Function must implement this discovery step and cache the result, since calling login.eloqua.com on every API request would add unnecessary latency. Once the Pod URL is established, Eloqua's REST API follows standard patterns: contacts (Eloqua's term for leads/contacts), email campaigns, forms, landing pages, and activity data are all accessible via REST endpoints.
Integration method
Oracle Eloqua requires a two-step connection process: first query login.eloqua.com to discover your instance's Pod URL (each Eloqua customer has their own dedicated pod), then use that Pod URL as the base for all subsequent API calls. A Supabase Edge Function handles this Pod URL discovery, caches the result, and proxies all Eloqua REST API calls using your credentials from Cloud Secrets.
Prerequisites
- A Lovable project with Lovable Cloud enabled
- An Oracle Eloqua account with API access (available on all Eloqua editions)
- Your Eloqua company name (the subdomain used for login), username, and password
- Basic understanding that Eloqua instances are hosted on different Pods — your API URL must be discovered, not assumed
- Optionally, Eloqua OAuth2 app credentials if using OAuth instead of Basic auth
Step-by-step guide
Gather your Eloqua credentials and understand the Pod pattern
Gather your Eloqua credentials and understand the Pod pattern
To use the Eloqua API, you need three pieces of information: your Eloqua Company Name (the organization identifier used when logging in — not the full company name, but the short code used as part of your login credentials), your API username, and your password. In Eloqua, your login username format is typically CompanyName\\Username — the Company Name is the part before the backslash. You can find it on the Eloqua login page or in your account settings. For Basic Auth, you will encode these as a Base64 string with the format 'CompanyName\\Username:Password'. For OAuth2 (more secure for production), you need to create an Eloqua application in Eloqua's API Console and obtain a Client ID and Client Secret. For this tutorial, we will use Basic Auth for simplicity — the same Edge Function pattern applies to OAuth2 by replacing the Authorization header. The critical insight about Eloqua's architecture is that your API URL is NOT fixed. Every Eloqua instance has a unique Pod URL. You MUST call https://login.eloqua.com/id with your Basic Auth credentials first — this returns a JSON object containing your instance's Pod URL in the urls.base field.
Pro tip: Test the Pod URL discovery before building anything else. Make a manual API call to https://login.eloqua.com/id with Basic Auth using your Eloqua Company\\Username:Password credentials. The response JSON includes urls.base — this is your API base URL.
Expected result: You know your Eloqua Company Name, username, and password. You have tested the Pod URL discovery endpoint manually and confirmed your Pod URL.
Store credentials in Cloud Secrets
Store credentials in Cloud Secrets
In your Lovable project, open the Cloud tab by clicking the plus (+) icon next to the preview panel. Navigate to the Secrets section and add three secrets: ELOQUA_COMPANY (your Eloqua company name — the short identifier used in login, before the backslash), ELOQUA_USERNAME (your API username, without the company prefix), and ELOQUA_PASSWORD (your Eloqua password). The Edge Function will combine these as 'CompanyName\\Username:Password', Base64-encode this string, and use it as a Basic Auth header. Click Save after each entry. If you are using Eloqua OAuth2 instead of Basic Auth, add ELOQUA_CLIENT_ID and ELOQUA_CLIENT_SECRET instead. The Pod URL itself will be discovered and cached at runtime — you do not need to store it as a secret since it is not sensitive and can be re-fetched from login.eloqua.com.
Pro tip: Use a dedicated Eloqua API user account rather than a personal user account. Create a user in Eloqua Admin → Users with a non-human email (like api-lovable@yourcompany.com) and assign it the minimum necessary role permissions.
Expected result: ELOQUA_COMPANY, ELOQUA_USERNAME, and ELOQUA_PASSWORD are stored in Cloud Secrets.
Create the Eloqua Pod discovery and proxy Edge Function
Create the Eloqua Pod discovery and proxy Edge Function
Build the Edge Function that implements the two-step Eloqua connection pattern. The function first checks if a cached Pod URL exists in Supabase. If not, it calls https://login.eloqua.com/id with Basic Auth to discover the Pod URL and caches it (Pod URLs are stable — they rarely change, so a 24-hour or even weekly cache is appropriate). With the Pod URL established, the function accepts proxy requests and forwards them to the correct Eloqua API endpoint at the Pod URL. Eloqua's REST API paths follow the pattern /api/REST/2.0/ for most operations. The function accepts method (GET/POST/PUT/DELETE), endpoint (the path under /api/REST/2.0/), and an optional body. Create a table called eloqua_config in Supabase to store the pod URL with a last_updated timestamp.
Create a Supabase Edge Function at supabase/functions/eloqua-proxy/index.ts. It should: (1) build Basic Auth from ELOQUA_COMPANY, ELOQUA_USERNAME, ELOQUA_PASSWORD secrets, (2) check for a cached Pod URL in a Supabase eloqua_config table, (3) if no cached URL, call https://login.eloqua.com/id to discover the Pod URL and cache it, (4) accept POST requests with method, endpoint (path under /api/REST/2.0/), and body, (5) proxy to the Pod URL + endpoint with Basic Auth and return results.
Paste this in Lovable chat
1import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'2import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'34const corsHeaders = {5 'Access-Control-Allow-Origin': '*',6 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',7}89async function getEloquaPodUrl(authHeader: string, supabase: any): Promise<string> {10 // Check cache first (valid for 7 days — pod URLs rarely change)11 const { data: cached } = await supabase12 .from('eloqua_config')13 .select('pod_url, updated_at')14 .eq('key', 'pod_url')15 .gt('updated_at', new Date(Date.now() - 7 * 24 * 3600000).toISOString())16 .single()1718 if (cached?.pod_url) return cached.pod_url1920 // Discover pod URL21 const resp = await fetch('https://login.eloqua.com/id', {22 headers: { 'Authorization': authHeader, 'Accept': 'application/json' },23 })2425 if (!resp.ok) throw new Error(`Eloqua login discovery failed: ${resp.status}`)2627 const data = await resp.json()28 const podUrl = data.urls?.base29 if (!podUrl) throw new Error('Could not determine Eloqua Pod URL from login response')3031 // Cache pod URL32 await supabase.from('eloqua_config').upsert({33 key: 'pod_url',34 pod_url: podUrl,35 updated_at: new Date().toISOString(),36 }, { onConflict: 'key' })3738 return podUrl39}4041serve(async (req) => {42 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })4344 try {45 const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!)4647 const company = Deno.env.get('ELOQUA_COMPANY')48 const username = Deno.env.get('ELOQUA_USERNAME')49 const password = Deno.env.get('ELOQUA_PASSWORD')5051 if (!company || !username || !password) throw new Error('Eloqua credentials not configured')5253 const credentials = btoa(`${company}\\${username}:${password}`)54 const authHeader = `Basic ${credentials}`5556 const podUrl = await getEloquaPodUrl(authHeader, supabase)5758 const { method, endpoint, body: requestBody } = await req.json()5960 const apiUrl = `${podUrl}/api/REST/2.0${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`6162 const response = await fetch(apiUrl, {63 method: method || 'GET',64 headers: {65 'Authorization': authHeader,66 'Content-Type': 'application/json',67 'Accept': 'application/json',68 },69 body: requestBody ? JSON.stringify(requestBody) : undefined,70 })7172 const responseData = await response.json()73 return new Response(JSON.stringify(responseData),74 { headers: { ...corsHeaders, 'Content-Type': 'application/json' } })75 } catch (error) {76 return new Response(JSON.stringify({ error: error.message }),77 { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } })78 }79})Pro tip: Create the eloqua_config table with columns: key (text primary key), pod_url (text), updated_at (timestamptz). The table stores the discovered Pod URL so the discovery step does not run on every API call.
Expected result: The eloqua-proxy Edge Function discovers your Eloqua Pod URL on first run and caches it. Test by calling it with endpoint='/contacts?count=1' — it should return your first Eloqua contact.
Implement contact creation from Lovable forms
Implement contact creation from Lovable forms
Wire up contact creation so that when a user submits a form in your Lovable app, their data is sent to Eloqua via the Edge Function. Eloqua's contact creation endpoint is POST /contacts. The request body uses Eloqua's field name format, which uses fieldDef IDs for custom fields but has named fields for standard attributes like firstName, lastName, emailAddress, company, and title. Creating a contact uses fieldValues format: an array of {id, value} objects for custom fields, plus top-level named fields for standard attributes. Create a form submit handler that constructs the Eloqua contact payload and calls the eloqua-proxy Edge Function. Handle the case where a contact already exists (Eloqua returns an error for duplicate email addresses — use a search-or-create pattern).
Create a contact form in Lovable that, when submitted, calls our eloqua-proxy Edge Function to create an Eloqua contact. The form should have first name, last name, email, company, and job title fields. POST to the Edge Function with endpoint='/contacts' and a body with firstName, lastName, emailAddress, company, and title. Handle duplicate email errors by showing a user-friendly message. Show a success state after creation.
Paste this in Lovable chat
1// Eloqua contact creation payload2const eloquaContact = {3 firstName: formData.firstName,4 lastName: formData.lastName,5 emailAddress: formData.email,6 company: formData.company,7 title: formData.jobTitle,8 // Custom fields use fieldValues array with Eloqua field definition IDs9 // fieldValues: [{ id: '100001', value: 'your custom value' }]10}1112// Call via Edge Function13const { data, error } = await supabase.functions.invoke('eloqua-proxy', {14 body: {15 method: 'POST',16 endpoint: '/contacts',17 body: eloquaContact,18 },19})Pro tip: Eloqua returns a 400 error with message 'Contact already exists' for duplicate emails. Implement a search step first: GET /contacts?search=emailAddress%3D{email} to check if the contact exists before attempting to create them.
Expected result: Form submissions from your Lovable app create Eloqua contacts visible in Eloqua's contact database within seconds.
Common use cases
Contact creation from Lovable app forms with campaign attribution
When a visitor fills out a form in your Lovable app (demo request, newsletter signup, content download), create them as an Eloqua contact via your Edge Function and associate them with the relevant Eloqua campaign. This ensures every lead generated by your app flows into Eloqua's lead management workflows immediately.
When a user submits our demo request form, call our eloqua-proxy Edge Function to create an Eloqua contact with their first name, last name, email, company, job title, and a custom field for demo_requested_date. Associate the contact with the campaign ID for our Q1 demand generation campaign. Show a confirmation message to the user after successful submission.
Copy this prompt to try it in Lovable
Campaign performance dashboard for marketing operations
Build a dashboard that pulls Eloqua email campaign performance data (sent, delivered, opened, clicked, bounced, unsubscribed) for all campaigns in the current quarter. Marketing operations teams can monitor campaign health without logging into Eloqua's reporting interface.
Create a campaign performance dashboard that calls our eloqua-proxy Edge Function to fetch all email campaigns with sendDate in the current quarter. Show a table with campaign name, send date, total sent, open rate, click rate, and bounce rate. Add a trend chart for open rate over time and a filter for campaign type.
Copy this prompt to try it in Lovable
Lead scoring and sales readiness dashboard
Query Eloqua contacts by lead score range and display a prioritized list for sales outreach. Include the contact's recent activities (email opens, web visits, form submissions) pulled from Eloqua's activity API to give sales reps context before calling.
Build a sales readiness dashboard that calls our Eloqua Edge Function to fetch contacts with lead score above 75. Display a table with contact name, company, email, lead score, and last activity date. Add a button to view each contact's recent Eloqua activities (emails opened, pages visited, forms submitted) in a side panel.
Copy this prompt to try it in Lovable
Troubleshooting
Edge Function cannot discover the Pod URL — login.eloqua.com returns 401
Cause: The Basic Auth credentials are incorrectly formatted. Eloqua requires 'CompanyName\Username:Password' format where the backslash separates company name from username.
Solution: Verify the format: ELOQUA_COMPANY should be just the company identifier (e.g., 'MyCompany'), not the full login string. The backslash in btoa(`${company}\\${username}:${password}`) is a JavaScript string escape — the actual encoded value will be 'MyCompany\Username:Password'. Test by manually constructing the Base64 string and calling login.eloqua.com in a REST client.
1// Correct credential format for Eloqua Basic Auth2const credentials = btoa(`${company}\\${username}:${password}`)3// Results in Base64 of: CompanyName\Username:PasswordPod URL discovery returns a URL but subsequent API calls return 404
Cause: The Pod URL path format may be slightly different. The login endpoint returns a base URL that may or may not include a trailing slash, and the API path needs to be appended correctly.
Solution: Log the full Pod URL returned from login.eloqua.com and verify the API URL being constructed. The Eloqua REST API path should be {podUrl}/api/REST/2.0/{endpoint}. Ensure no double slashes occur at the join point.
1// Ensure clean URL construction2const podUrl = discoveredUrl.replace(/\/+$/, '') // Remove trailing slashes3const apiUrl = `${podUrl}/api/REST/2.0${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`Eloqua API returns 'Contact already exists' when trying to create a contact
Cause: Eloqua uses email address as a unique identifier for contacts. If a contact with that email already exists, a POST to /contacts fails.
Solution: Implement a search-before-create pattern: first GET /contacts?search=emailAddress%3D{email} to check if the contact exists. If found, use PUT /contacts/{id} to update. If not found, use POST /contacts to create. This upsert pattern prevents duplicate contact errors.
1// Search for existing contact before creating2const searchResp = await eloquaProxy({ method: 'GET', endpoint: `/contacts?search=emailAddress%3D${encodeURIComponent(email)}` })3const existing = searchResp.elements?.[0]4if (existing) {5 // Update existing contact6 await eloquaProxy({ method: 'PUT', endpoint: `/contacts/${existing.id}`, body: contactData })7} else {8 // Create new contact9 await eloquaProxy({ method: 'POST', endpoint: '/contacts', body: contactData })10}Eloqua API returns responses but custom field values are not being saved
Cause: Eloqua custom fields require specific field definition IDs in the fieldValues array, not field names. Using field names instead of IDs causes the values to be silently ignored.
Solution: Retrieve your Eloqua custom field definition IDs by calling GET /contacts/fields — this returns all contact fields with their IDs. Use these numeric IDs in the fieldValues array: [{id: '100001', value: 'custom value'}]. Field IDs are specific to your Eloqua instance.
1// Get field IDs first: GET /contacts/fields2// Then use IDs in fieldValues3body: {4 emailAddress: email,5 fieldValues: [6 { id: '100001', value: 'custom field value' }, // Use actual Eloqua field ID7 ]8}Best practices
- Always implement the Pod URL discovery step before any other Eloqua API call — never hardcode the pod URL as it can change during account migrations
- Cache the Pod URL in Supabase with a weekly TTL — pod URLs are stable and re-discovering on every request adds unnecessary latency
- Use a dedicated Eloqua API user account with minimum necessary role permissions rather than an admin account
- Implement the search-before-create pattern for contact operations to handle the duplicate email error gracefully
- Retrieve Eloqua field definition IDs once via GET /contacts/fields and store them as configuration in your app rather than hardcoding guessed IDs
- Eloqua has strict API rate limits (typically 2,000 requests per day on some editions) — implement aggressive caching for read operations like campaign and contact lookups
- Use Eloqua's bulk API endpoints for large data operations (syncing thousands of contacts) rather than individual REST calls which would quickly exhaust rate limits
- Test your integration against Eloqua's sandbox (Oracle Test Automation Environment) if your license includes it before connecting to production data
Alternatives
Pardot is Salesforce's B2B marketing automation for organizations in the Salesforce ecosystem, while Eloqua is Oracle's enterprise platform — the choice typically depends on which CRM your organization uses.
Marketo is Adobe's standalone enterprise marketing platform with its own simpler OAuth2 authentication, while Eloqua is Oracle's platform with the unique Pod URL architecture requiring additional discovery steps.
HubSpot offers marketing automation with a simpler REST API and more accessible pricing, while Eloqua is designed for large enterprises with complex campaign requirements and Oracle ecosystem integration.
Frequently asked questions
What is an Eloqua Pod and why does it matter for API integration?
Eloqua deploys each customer organization on a dedicated infrastructure cluster called a Pod. Unlike multi-tenant SaaS platforms where all customers share the same API URL, your Eloqua instance has its own unique domain (e.g., p01.eloqua.com, p04.eloqua.com). This means there is no single API base URL that works for all Eloqua customers. You must query login.eloqua.com with your credentials first to discover your instance's Pod URL — this is mandatory and cannot be skipped.
Should I use Basic Auth or OAuth2 for the Eloqua API?
For production integrations, Oracle recommends OAuth2 over Basic Auth because it allows credential rotation without changing application code and supports finer-grained permission scopes. However, Basic Auth is simpler to implement and works reliably. For a Lovable integration where credentials are stored in secure Cloud Secrets (not in frontend code), Basic Auth is acceptable. If your organization requires OAuth2 for compliance reasons, the Edge Function pattern supports it — you just replace the Authorization header construction with a token exchange flow.
What is Eloqua's API rate limit?
Eloqua's REST API rate limits vary by edition. Standard limits are approximately 2,000 API requests per day for the basic REST API. Eloqua also offers a Bulk API designed for high-volume operations that has separate, more generous limits. For dashboards that need to display data frequently, implementing Supabase caching with appropriate TTLs is essential to stay within rate limits.
Can I use Eloqua's tracking script in a Lovable app?
Yes. Eloqua provides a JavaScript tracking script that identifies anonymous visitors and associates their web activity with known contacts. Add the Eloqua tracking script to your Lovable app's index.html file. When a known contact is identified (e.g., they submit a form), their web activity history is associated with their Eloqua contact record, feeding behavioral scoring. The script URL is typically at your Pod URL: {podUrl}/e/f2.
How do I find my Eloqua custom field IDs?
Call GET /api/REST/2.0/contacts/fields through your eloqua-proxy Edge Function. The response returns all contact field definitions including their numeric IDs, names, and data types. Store these IDs in your application configuration or a Supabase table rather than hardcoding them, since field IDs are specific to your Eloqua instance and can change if fields are deleted and recreated.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation