This workflow follows the HTTP Request → Postgres recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "WebinarJam \u2192 Supabase Data Sync",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"id": "sync-trigger",
"name": "Sync Every 6 Hours",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
300
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.webinarjam.com/everwebinar/registrants",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/x-www-form-urlencoded"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "api_key",
"value": "={{ $credentials.webinarjam.apiKey }}"
},
{
"name": "webinar_id",
"value": "13"
}
]
},
"options": {
"timeout": 60000,
"retry": {
"enabled": true,
"maxTries": 3
}
}
},
"id": "fetch-registrants",
"name": "Fetch All Registrants",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
460,
300
],
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Parse WebinarJam API response and prepare for Supabase\nconst response = $input.first().json;\n\nif (!response.status || response.status !== 'success') {\n throw new Error(`WebinarJam API Error: ${response.message || 'Unknown error'}`);\n}\n\nconst registrants = response.registrants || [];\nconst totalPages = Math.ceil((response.total_items || 0) / (response.per_page || 100));\n\nconsole.log(`Processing ${registrants.length} registrants from page 1 of ${totalPages}`);\n\n// Transform each registrant for Supabase\nconst transformedRegistrants = registrants.map(registrant => {\n // Parse dates safely\n const parseDate = (dateStr) => {\n if (!dateStr || dateStr === '0000-00-00 00:00:00') return null;\n try {\n return new Date(dateStr).toISOString();\n } catch (e) {\n return null;\n }\n };\n\n // Parse intervals (like \"01:23:45\" to PostgreSQL INTERVAL)\n const parseInterval = (timeStr) => {\n if (!timeStr || timeStr === '00:00:00') return null;\n return timeStr; // PostgreSQL will handle HH:MM:SS format\n };\n\n return {\n webinarjam_id: parseInt(registrant.id) || 0,\n lead_id: parseInt(registrant.lead_id) || null,\n webinar_id: parseInt(registrant.webinar_id) || 0,\n schedule_id: parseInt(registrant.schedule_id) || 0,\n event_id: parseInt(registrant.event_id) || null,\n \n // Contact Info\n email: registrant.email || '',\n first_name: registrant.first_name || '',\n last_name: registrant.last_name || '',\n phone_country_code: registrant.phone_country_code || null,\n phone_number: registrant.phone_number || null,\n \n // Webinar Details\n webinar_name: registrant.webinar_name || '',\n schedule: registrant.schedule || '',\n event_date: parseDate(registrant.event_date),\n signup_date: parseDate(registrant.signup_date),\n \n // Attendance Status\n attended_live: registrant.attended_live === '1' || registrant.attended_live === true,\n date_live: parseDate(registrant.date_live),\n entered_live: parseInterval(registrant.entered_live),\n time_live: parseInterval(registrant.time_live),\n \n // Purchase Data\n purchased_live: registrant.purchased_live === '1' || registrant.purchased_live === true,\n revenue_live: parseFloat(registrant.revenue_live) || 0,\n \n // Replay Data\n attended_replay: registrant.attended_replay === '1' || registrant.attended_replay === true,\n date_replay: parseDate(registrant.date_replay),\n time_replay: parseInterval(registrant.time_replay),\n purchased_replay: registrant.purchased_replay === '1' || registrant.purchased_replay === true,\n revenue_replay: parseFloat(registrant.revenue_replay) || 0,\n \n // Subscription Status\n subscribed: registrant.subscribed !== '0' && registrant.subscribed !== false,\n gdpr_status: registrant.gdpr_status || null,\n gdpr_communications: registrant.gdpr_communications || null,\n \n // UTM Tracking\n utm_source: registrant.utm_source || null,\n utm_medium: registrant.utm_medium || null,\n utm_campaign: registrant.utm_campaign || null,\n utm_term: registrant.utm_term || null,\n utm_content: registrant.utm_content || null\n };\n});\n\nreturn {\n registrants: transformedRegistrants,\n pagination: {\n current_page: 1,\n total_pages: totalPages,\n total_items: response.total_items || 0,\n per_page: response.per_page || 100\n },\n sync_timestamp: new Date().toISOString()\n};"
},
"id": "transform-data",
"name": "Transform Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
680,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "=-- Batch upsert registrants into Supabase\n{% for registrant in $json.registrants %}\nSELECT upsert_webinar_registrant(\n {{ registrant.webinarjam_id }},\n {% if registrant.lead_id %}{{ registrant.lead_id }}{% else %}NULL{% endif %},\n {{ registrant.webinar_id }},\n {{ registrant.schedule_id }},\n {% if registrant.event_id %}{{ registrant.event_id }}{% else %}NULL{% endif %},\n '{{ registrant.email | replace(\"'\", \"''\") }}',\n '{{ registrant.first_name | replace(\"'\", \"''\") }}',\n '{{ registrant.last_name | replace(\"'\", \"''\") }}',\n {% if registrant.phone_country_code %}'{{ registrant.phone_country_code }}'{% else %}NULL{% endif %},\n {% if registrant.phone_number %}'{{ registrant.phone_number }}'{% else %}NULL{% endif %},\n '{{ registrant.webinar_name | replace(\"'\", \"''\") }}',\n '{{ registrant.schedule | replace(\"'\", \"''\") }}',\n {% if registrant.event_date %}'{{ registrant.event_date }}'{% else %}NULL{% endif %},\n {% if registrant.signup_date %}'{{ registrant.signup_date }}'{% else %}NULL{% endif %},\n {{ registrant.attended_live }},\n {% if registrant.date_live %}'{{ registrant.date_live }}'{% else %}NULL{% endif %},\n {% if registrant.entered_live %}'{{ registrant.entered_live }}'{% else %}NULL{% endif %},\n {% if registrant.time_live %}'{{ registrant.time_live }}'{% else %}NULL{% endif %},\n {{ registrant.purchased_live }},\n {{ registrant.revenue_live }},\n {{ registrant.attended_replay }},\n {% if registrant.date_replay %}'{{ registrant.date_replay }}'{% else %}NULL{% endif %},\n {% if registrant.time_replay %}'{{ registrant.time_replay }}'{% else %}NULL{% endif %},\n {{ registrant.purchased_replay }},\n {{ registrant.revenue_replay }},\n {{ registrant.subscribed }},\n {% if registrant.gdpr_status %}'{{ registrant.gdpr_status | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.gdpr_communications %}'{{ registrant.gdpr_communications | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.utm_source %}'{{ registrant.utm_source | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.utm_medium %}'{{ registrant.utm_medium | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.utm_campaign %}'{{ registrant.utm_campaign | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.utm_term %}'{{ registrant.utm_term | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.utm_content %}'{{ registrant.utm_content | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %}\n) as upserted_id;\n{% endfor %}",
"options": {}
},
"id": "sync-to-supabase",
"name": "Sync to Supabase",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
900,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "check-more-pages",
"leftValue": "={{ $('transform-data').item.json.pagination.total_pages }}",
"rightValue": 1,
"operator": {
"type": "number",
"operation": "gt",
"rightType": "number"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "check-pagination",
"name": "More Pages?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1120,
300
]
},
{
"parameters": {
"jsCode": "// Create pagination loop for remaining pages\nconst pagination = $('transform-data').item.json.pagination;\nconst totalPages = pagination.total_pages;\nconst startPage = 2; // We already processed page 1\n\nconst pageRequests = [];\n\nfor (let page = startPage; page <= totalPages; page++) {\n pageRequests.push({\n page: page,\n total_pages: totalPages\n });\n}\n\nconsole.log(`Creating ${pageRequests.length} additional page requests (pages ${startPage}-${totalPages})`);\n\nreturn pageRequests.map(req => ({ json: req }));"
},
"id": "create-page-requests",
"name": "Create Page Requests",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
200
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.webinarjam.com/everwebinar/registrants",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/x-www-form-urlencoded"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "api_key",
"value": "={{ $credentials.webinarjam.apiKey }}"
},
{
"name": "webinar_id",
"value": "13"
},
{
"name": "page",
"value": "={{ $json.page }}"
}
]
},
"options": {
"timeout": 60000,
"retry": {
"enabled": true,
"maxTries": 3
}
}
},
"id": "fetch-additional-pages",
"name": "Fetch Additional Pages",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1560,
200
],
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Transform additional page data for Supabase\nconst response = $input.item.json;\n\nif (!response.status || response.status !== 'success') {\n console.log(`Warning: Page ${$input.item.json.page || 'unknown'} failed: ${response.message || 'Unknown error'}`);\n return { json: { registrants: [], page: $input.item.json.page || 0 } };\n}\n\nconst registrants = response.registrants || [];\nconst currentPage = response.current_page || $input.item.json.page || 0;\n\nconsole.log(`Processing ${registrants.length} registrants from page ${currentPage}`);\n\n// Transform each registrant for Supabase (same logic as main transform)\nconst transformedRegistrants = registrants.map(registrant => {\n const parseDate = (dateStr) => {\n if (!dateStr || dateStr === '0000-00-00 00:00:00') return null;\n try {\n return new Date(dateStr).toISOString();\n } catch (e) {\n return null;\n }\n };\n\n const parseInterval = (timeStr) => {\n if (!timeStr || timeStr === '00:00:00') return null;\n return timeStr;\n };\n\n return {\n webinarjam_id: parseInt(registrant.id) || 0,\n lead_id: parseInt(registrant.lead_id) || null,\n webinar_id: parseInt(registrant.webinar_id) || 0,\n schedule_id: parseInt(registrant.schedule_id) || 0,\n event_id: parseInt(registrant.event_id) || null,\n \n email: registrant.email || '',\n first_name: registrant.first_name || '',\n last_name: registrant.last_name || '',\n phone_country_code: registrant.phone_country_code || null,\n phone_number: registrant.phone_number || null,\n \n webinar_name: registrant.webinar_name || '',\n schedule: registrant.schedule || '',\n event_date: parseDate(registrant.event_date),\n signup_date: parseDate(registrant.signup_date),\n \n attended_live: registrant.attended_live === '1' || registrant.attended_live === true,\n date_live: parseDate(registrant.date_live),\n entered_live: parseInterval(registrant.entered_live),\n time_live: parseInterval(registrant.time_live),\n \n purchased_live: registrant.purchased_live === '1' || registrant.purchased_live === true,\n revenue_live: parseFloat(registrant.revenue_live) || 0,\n \n attended_replay: registrant.attended_replay === '1' || registrant.attended_replay === true,\n date_replay: parseDate(registrant.date_replay),\n time_replay: parseInterval(registrant.time_replay),\n purchased_replay: registrant.purchased_replay === '1' || registrant.purchased_replay === true,\n revenue_replay: parseFloat(registrant.revenue_replay) || 0,\n \n subscribed: registrant.subscribed !== '0' && registrant.subscribed !== false,\n gdpr_status: registrant.gdpr_status || null,\n gdpr_communications: registrant.gdpr_communications || null,\n \n utm_source: registrant.utm_source || null,\n utm_medium: registrant.utm_medium || null,\n utm_campaign: registrant.utm_campaign || null,\n utm_term: registrant.utm_term || null,\n utm_content: registrant.utm_content || null\n };\n});\n\nreturn {\n json: {\n registrants: transformedRegistrants,\n page: currentPage\n }\n};"
},
"id": "transform-additional-pages",
"name": "Transform Additional Pages",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
200
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "=-- Batch upsert additional page registrants\n{% for registrant in $json.registrants %}\nSELECT upsert_webinar_registrant(\n {{ registrant.webinarjam_id }},\n {% if registrant.lead_id %}{{ registrant.lead_id }}{% else %}NULL{% endif %},\n {{ registrant.webinar_id }},\n {{ registrant.schedule_id }},\n {% if registrant.event_id %}{{ registrant.event_id }}{% else %}NULL{% endif %},\n '{{ registrant.email | replace(\"'\", \"''\") }}',\n '{{ registrant.first_name | replace(\"'\", \"''\") }}',\n '{{ registrant.last_name | replace(\"'\", \"''\") }}',\n {% if registrant.phone_country_code %}'{{ registrant.phone_country_code }}'{% else %}NULL{% endif %},\n {% if registrant.phone_number %}'{{ registrant.phone_number }}'{% else %}NULL{% endif %},\n '{{ registrant.webinar_name | replace(\"'\", \"''\") }}',\n '{{ registrant.schedule | replace(\"'\", \"''\") }}',\n {% if registrant.event_date %}'{{ registrant.event_date }}'{% else %}NULL{% endif %},\n {% if registrant.signup_date %}'{{ registrant.signup_date }}'{% else %}NULL{% endif %},\n {{ registrant.attended_live }},\n {% if registrant.date_live %}'{{ registrant.date_live }}'{% else %}NULL{% endif %},\n {% if registrant.entered_live %}'{{ registrant.entered_live }}'{% else %}NULL{% endif %},\n {% if registrant.time_live %}'{{ registrant.time_live }}'{% else %}NULL{% endif %},\n {{ registrant.purchased_live }},\n {{ registrant.revenue_live }},\n {{ registrant.attended_replay }},\n {% if registrant.date_replay %}'{{ registrant.date_replay }}'{% else %}NULL{% endif %},\n {% if registrant.time_replay %}'{{ registrant.time_replay }}'{% else %}NULL{% endif %},\n {{ registrant.purchased_replay }},\n {{ registrant.revenue_replay }},\n {{ registrant.subscribed }},\n {% if registrant.gdpr_status %}'{{ registrant.gdpr_status | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.gdpr_communications %}'{{ registrant.gdpr_communications | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.utm_source %}'{{ registrant.utm_source | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.utm_medium %}'{{ registrant.utm_medium | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.utm_campaign %}'{{ registrant.utm_campaign | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.utm_term %}'{{ registrant.utm_term | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %},\n {% if registrant.utm_content %}'{{ registrant.utm_content | replace(\"'\", \"''\") }}'{% else %}NULL{% endif %}\n) as upserted_id_page_{{ $json.page }};\n{% endfor %}",
"options": {}
},
"id": "sync-additional-pages",
"name": "Sync Additional Pages",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
2000,
200
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Final sync summary\nSELECT \n COUNT(*) as total_synced,\n COUNT(CASE WHEN attended_live THEN 1 END) as attended_count,\n COUNT(CASE WHEN NOT attended_live THEN 1 END) as no_show_count,\n MAX(last_synced_at) as last_sync_time,\n webinar_name\nFROM webinar_registrants \nWHERE webinar_id = 13\nGROUP BY webinar_name;",
"options": {}
},
"id": "sync-summary",
"name": "Sync Summary",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
1340,
400
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Create comprehensive sync report\nconst syncSummary = $('sync-summary').first().json;\nconst initialSync = $('sync-to-supabase').first().json;\nconst additionalPagesResult = $('sync-additional-pages').all();\n\nconst report = {\n sync_completed_at: new Date().toISOString(),\n webinar_id: 13,\n total_registrants_synced: syncSummary.total_synced || 0,\n attended_live_count: syncSummary.attended_count || 0,\n no_show_count: syncSummary.no_show_count || 0,\n attendance_rate: syncSummary.total_synced > 0 ? \n Math.round((syncSummary.attended_count / syncSummary.total_synced) * 100 * 100) / 100 : 0,\n webinar_name: syncSummary.webinar_name || 'Unknown Webinar',\n pages_processed: 1 + (additionalPagesResult?.length || 0),\n database_ready: true,\n next_sync_in: '6 hours'\n};\n\nconsole.log('\ud83d\udcca SYNC COMPLETED');\nconsole.log(`\u2705 ${report.total_registrants_synced} registrants synced`);\nconsole.log(`\ud83d\udc65 ${report.attended_live_count} attended live (${report.attendance_rate}%)`);\nconsole.log(`\u274c ${report.no_show_count} no-shows`);\nconsole.log(`\ud83d\udcc4 ${report.pages_processed} pages processed`);\nconsole.log(`\ud83d\udd04 Next sync in ${report.next_sync_in}`);\n\nreturn { json: report };"
},
"id": "final-report",
"name": "Final Report",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
400
]
}
],
"connections": {
"sync-trigger": {
"main": [
[
{
"node": "fetch-registrants",
"type": "main",
"index": 0
}
]
]
},
"fetch-registrants": {
"main": [
[
{
"node": "transform-data",
"type": "main",
"index": 0
}
]
]
},
"transform-data": {
"main": [
[
{
"node": "sync-to-supabase",
"type": "main",
"index": 0
}
]
]
},
"sync-to-supabase": {
"main": [
[
{
"node": "check-pagination",
"type": "main",
"index": 0
}
]
]
},
"check-pagination": {
"main": [
[
{
"node": "create-page-requests",
"type": "main",
"index": 0
}
],
[
{
"node": "sync-summary",
"type": "main",
"index": 0
}
]
]
},
"create-page-requests": {
"main": [
[
{
"node": "fetch-additional-pages",
"type": "main",
"index": 0
}
]
]
},
"fetch-additional-pages": {
"main": [
[
{
"node": "transform-additional-pages",
"type": "main",
"index": 0
}
]
]
},
"transform-additional-pages": {
"main": [
[
{
"node": "sync-additional-pages",
"type": "main",
"index": 0
}
]
]
},
"sync-additional-pages": {
"main": [
[
{
"node": "sync-summary",
"type": "main",
"index": 0
}
]
]
},
"sync-summary": {
"main": [
[
{
"node": "final-report",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "sync-v1.0",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "webinarjam-supabase-sync",
"tags": [
{
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z",
"id": "data-sync",
"name": "data-sync"
},
{
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z",
"id": "webinarjam",
"name": "webinarjam"
},
{
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z",
"id": "supabase",
"name": "supabase"
}
]
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpBasicAuthpostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
How this works
Effortlessly keep your WebinarJam attendee data up to date in Supabase without manual exports, ensuring your analytics and follow-up lists remain accurate and actionable for marketing teams or event organisers. This workflow fetches registrant details via HTTP requests to WebinarJam's API, transforms the data for compatibility, and syncs it directly into your Supabase database using Postgres nodes. The key step involves paginated fetching to capture all records efficiently, running every six hours to balance freshness with resource use.
Use this when managing recurring webinars with growing attendee lists that need seamless integration into a Supabase-powered CRM or reporting system, particularly if you're already using these tools for customer insights. Avoid it for one-off events or when real-time syncing is essential, as the scheduled trigger introduces a delay. Common variations include adjusting the cron interval for daily runs or adding error-handling nodes to notify via email on sync failures.
About this workflow
WebinarJam → Supabase Data Sync. Uses scheduleTrigger, httpRequest, postgres. Scheduled trigger; 11 nodes.
Source: https://github.com/Djuty/webinarjam-lightning-attendee-system/blob/1839241defe280a474818cc61e972798616049af/workflows/data-sync.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Disparador 1.8. Uses itemLists, postgres, emailSend, httpRequest. Scheduled trigger; 85 nodes.
공유회_알림톡_크론. Uses postgres, httpRequest, n8n-nodes-solapi. Scheduled trigger; 39 nodes.
QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.
QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.
QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.