Meetup planner — create, publish, and manage events across Luma + Eventbrite + LinkedIn via n8n, with AI-generated cover images, cross-platform search, and normalized event/guest lookups. Use whenever the user mentions hosting, scheduling, or running an event or meetup; searching events; checking conflicts or duplicates; looking up attendees or guest lists; generating a cover image or Canva design for an event; or posting an event announcement — even if they don't say 'event' explicitly.
Create and manage events across Luma and Eventbrite via n8n.
Three environment variables are required before making any calls:
| Variable | What it is |
|---|---|
N8N_HOST | Base URL of the n8n instance (no trailing slash). For this deployment: https://n8n.yvrbtclabs.dev |
N8N_WEBHOOK_AUTH | Webhook auth secret (hex string) validated by the nginx reverse proxy in front of n8n. Not the n8n API key (JWT) — that one is separate and this skill does not use it. |
OUTSCRAPER_API_KEY | API key for Outscraper venue lookups (used in Step 2). If unavailable, skip venue lookup and proceed without a place_id. |
Every /webhook/* request must include:
X-API-Key: $N8N_WEBHOOK_AUTH
A missing or invalid key returns 401 Unauthorized from nginx as HTML, not JSON — if you parse the response as JSON on a 401, you'll get a parse error. Treat any non-2xx with Content-Type: text/html as an auth/proxy failure, not an API error.
The workflow below shows the most common curl examples inline, but they don't cover every optional field or edge case. Read references/api-reference.md (has a table of contents — jump to the relevant endpoint) whenever you need the full request/response schema, an endpoint not shown inline (direct Luma/Eventbrite creation, image combine/edit, Canva helpers), or the exact error shapes.
Validate → Draft → Human Review → Publish. Every event goes through this flow as an interactive Telegram conversation.
Ask the user for:
Run in parallel, wait for both before proceeding. Stop and tell the user if either fails.
Outscraper venue lookup:
Important: the submit URL and the results URL use different domains.
# 1. Start the search (async) — note: api.app.outscraper.com
curl -s -H "X-API-KEY: $OUTSCRAPER_API_KEY" \
"https://api.app.outscraper.com/maps/search-v3?query=Stanley+Park+Vancouver+BC&limit=1"
Returns { "id": "...", "results_location": "https://api.outscraper.cloud/requests/<id>", "status": "Pending" }.
# 2. Poll for results (wait 3-5 seconds) — note: api.outscraper.cloud
sleep 4
curl -s -H "X-API-KEY: $OUTSCRAPER_API_KEY" \
"https://api.outscraper.cloud/requests/<id>"
place_id for event-publish.working_hours against the event day/time. If closed, stop and tell the user.Conflict + duplicate check:
curl -s -X POST -H "X-API-Key: $N8N_WEBHOOK_AUTH" \
-H "Content-Type: application/json" \
"$N8N_HOST/webhook/event-check-conflicts" \
-d '{
"name": "Bitdevs",
"start_at": "2026-04-01T02:00:00.000Z",
"end_at": "2026-04-01T04:00:00.000Z",
"city": "Vancouver"
}'
city whenever you have it — it filters out unrelated same-day candidates from other regions.conflicts (time overlap — blocking), similar (same-day events with overlapping topic keywords — likely duplicates, surface to user before publishing), same_day (same day, no overlap, no topic match — informational).has_issues as the single "block or warn" flag. Block only on conflicts; for similar, ask the user to confirm before proceeding.Only after Step 2 passes. Call both at the same time:
Draft:
curl -s -X POST -H "X-API-Key: $N8N_WEBHOOK_AUTH" \
-H "Content-Type: application/json" \
"$N8N_HOST/webhook/event-draft" \
-d '{
"topic": "Automating your pets life with AI",
"date": "Wednesday April 8th 2026, 7pm",
"location": "Stanley Park, Vancouver, BC",
"notes": "Optional extra context",
"cc": "[email protected]"
}'
cc (optional) — grants Google Doc editor access. Omit if no email available. Don't ask for it.Returns: { "success": true, "title": "...", "doc_url": "https://docs.google.com/document/d/.../edit" }
Cover image illustration(s):
Generate illustration image options. Call /webhook/event-image-parts once — it returns multiple options in a single call. Each call costs real money for image generation, so don't regenerate unless the user explicitly asks for more options.
curl -s -X POST -H "X-API-Key: $N8N_WEBHOOK_AUTH" \
-H "Content-Type: application/json" \
"$N8N_HOST/webhook/event-image-parts" \
-d '{
"event_name": "AI Pet Tech: From Kibble to Code",
"description": "Learn how to build AI agents for pet care automation",
"date": "Apr 8 (Wed)",
"time": "7pm",
"venue": "Stanley Park"
}'
| Field | Required | Notes |
|---|---|---|
event_name | Yes | Event title — guides the illustration |
description | Yes | Context for the illustration scene |
date | Yes | Date text for the cover label |
time | Yes | Time text for the cover label |
venue | Yes | Venue text for the cover label |
feedback | No | Optional text to steer regeneration |
calendar_api_id | No | Luma calendar API ID (server has a default) |
Returns:
{
"success": true,
"event_name": "AI Pet Tech: From Kibble to Code",
"count": 3,
"images": [
{
"index": 1,
"public_url": "$N8N_HOST/event-images/...",
"canva_edit_url": "https://www.canva.com/...",
"canva_design_id": "...",
"canva_asset_id": "...",
"drive_file_id": "...",
"drive_url": "..."
}
],
"template_url": "https://www.canva.com/design/DAHGR0eUReA/.../edit",
"export_url": "$N8N_HOST/webhook/canva-export-design"
}
images[] contains 3 illustration variations by default — iterate over it for the previews. Each generated image is 1910x1112px with a #F5F5E9 background, designed to be placed into the Canva template.
Send the user all three of these items together — the review workflow below breaks if any one is missing:
images[] as a separate Telegram message, using its public_url for inline previewtemplate_url — the Canva cover template. The user picks their favourite image from the previews, opens the template, places their chosen image into it, updates the event title/date/text, then tells you they're done.Skip canva_edit_url for individual images — sending those too just clutters the review and tempts the user to edit a preview instead of the template.
The user's workflow: preview images in Telegram -> pick a favourite -> open template_url -> place chosen image into template -> edit text -> say "done" or "publish".
Save the doc_url, template_url, image public_urls, and the title.
When the user says "done" or "publish", export the finished Canva template (NOT an individual image — the template is the final cover):
curl -s -X POST -H "X-API-Key: $N8N_WEBHOOK_AUTH" \
-H "Content-Type: application/json" \
"$N8N_HOST/webhook/canva-export-design" \
-d '{
"design_id": "DAHGR0eUReA",
"page": 1
}'
The design_id is always the fixed template (DAHGR0eUReA) — do not extract per-variation IDs from images[].canva_design_id; those are the upload designs, not the cover to export.
Returns:
{
"job": {
"status": "success",
"urls": ["https://export-download.canva.com/..."]
}
}
urls[0] is a temporary download link for the finished 1920x1920 PNG (expires in a few hours).
Upload the exported image to Drive for a permanent URL:
curl -s -X POST -H "X-API-Key: $N8N_WEBHOOK_AUTH" \
-H "Content-Type: application/json" \
"$N8N_HOST/webhook/upload-image" \
-d '{
"url": "<export URL from canva-export-design>",
"file_name": "event-cover-final.png"
}'
Use the returned public_url as the final cover image for publish.
curl -s --max-time 200 -X POST -H "X-API-Key: $N8N_WEBHOOK_AUTH" \
-H "Content-Type: application/json" \
"$N8N_HOST/webhook/event-publish" \
-d '{
"doc_id": "the-google-doc-id",
"file_id": "1HPP2gy_xxx",
"image_url": "https://drive.google.com/uc?id=1HPP2gy_xxx&export=download",
"place_id": "ChIJQ9pJmH5xhlQRe_nvreYL37k"
}'
doc_id — extract from doc_url (between /d/ and /edit)file_id — Google Drive file ID of the final cover image (from the Canva export upload). Used by the Luma/Eventbrite/Canva cover pipeline.image_url — public HTTPS URL of the same cover image. Required — LinkedIn's post step fetches this directly and will crash if it's missing. Construct it from the file_id as https://drive.google.com/uc?id=<file_id>&export=download, or use the public_url returned from upload-image.place_id (optional) — from Outscraper. Omit for virtual events.Both file_id AND image_url are required when publishing with a cover. They are not interchangeable — file_id drives the Luma/EB/Canva cover attachment, while image_url is what LinkedIn's image fetcher uses for the post. To publish without a cover, omit both.
Important: this call takes 60–90 seconds to return. Use a timeout of at least 180 seconds. The server creates the Luma + Eventbrite + LinkedIn events AND uploads/attaches the cover image on all platforms AND creates a Canva design before returning. When this response comes back, everything is live and the cover is attached — you can immediately share links with the user.
Response includes:
luma_url, luma_event_id, eventbrite_url, eventbrite_event_id, linkedin_url, linkedin_success, doc_url, sheet_urlcover_url — Luma CDN URL of the uploaded covercanva_design_id, canva_edit_url, canva_view_url — the draft Canva design for manual tweakseb_cover_success, eb_cover_error — per-platform cover statusluma_error, eventbrite_error — per-platform publish errorsError handling:
success: true means the event is published. Individual platforms may still have errors — check luma_error, eventbrite_error, linkedin_success.success: true but cover_url is empty or eb_cover_error is populated, the event is live but the cover didn't stick — tell the user the cover didn't apply, don't claim "your event is ready" as if everything worked.Share the event links with the user.
/webhook/telegram-announce with text and/or image_url to post to configured channel(s)./webhook/send-email with a summary of all links.Standalone tool — modify an existing image with a text prompt. Not part of the publish flow; use when the user wants to tweak a cover that's already been chosen or published.
curl -s -X POST -H "X-API-Key: $N8N_WEBHOOK_AUTH" \
-H "Content-Type: application/json" \
"$N8N_HOST/webhook/event-image-edit" \
-d '{
"images": ["<url of image to edit>"],
"prompt": "make the background darker",
"event_name": "AI Pet Tech: From Kibble to Code"
}'
Returns an array of edited images, each with file_id, drive_url, public_url, thumbnail_url.
Everything below goes through platform-agnostic orchestrators. The skill does not branch on Luma vs Eventbrite — n8n fans out and returns normalized results.
| Action | Endpoint |
|---|---|
| Search events (cross-platform) | GET /webhook/event-search-all |
| Check conflicts + duplicates | POST /webhook/event-check-conflicts |
| Get event details (by URL or ID) | POST /webhook/event-get |
| Get event guest list | POST /webhook/event-guests |
| List my events (past or future) | POST /webhook/my-events |
| Upload image to Drive | POST /webhook/upload-image |
| Post to LinkedIn | POST /webhook/linkedin-post |
| Post to Telegram channel | POST /webhook/telegram-announce |
| Send email | POST /webhook/send-email |
| Create on Luma only (escape hatch) | POST /webhook/luma-create-event |
| Create on Eventbrite only (escape hatch) | POST /webhook/eventbrite-create-event |
| Update cover on existing event | POST /webhook/event-set-cover |
Prefer event-publish for normal creation — it handles both platforms, LinkedIn, signup tracking, AND cover image attachment in one call.
event-set-cover is only for updating a cover on an event that's already published. Do NOT use it as part of the create-event flow — event-publish handles cover attachment automatically.
event-get, event-guests, my-events, and event-search-all all return the same event shape. Safe to build against:
{
"platform": "luma" | "eventbrite",
"event_id": "...",
"name": "...",
"start_at": "ISO UTC",
"end_at": "ISO UTC",
"url": "...",
"cover_url": "..."
}
event-get additionally populates description, venue, organizer, show_guest_list, and a raw passthrough (avoid relying on raw). event-guests returns guests[] with guest_id (stable per-signup ID — use for diffing), user_id, name, email, ticket_count. my-events returns a merged, chronologically-sorted events[] plus a per-platform counts map; a single-platform failure surfaces in errors without blocking the other.
Accept URLs or IDs in url_or_id — the orchestrator auto-detects lu.ma URLs, Luma slugs, evt-*/calev-* IDs, Eventbrite URLs, and numeric Eventbrite event IDs.
| Endpoint | Purpose |
|---|---|
/webhook/canva-create-design | Create a new design (poster, presentation, doc, etc.) |
/webhook/canva-export-design | Export design to PNG/JPG/PDF/PPTX/MP4 |
/webhook/canva-get-design | Fetch design metadata by ID |
/webhook/canva-list-designs | List or search designs |
/webhook/canva-upload-asset | Upload image URL into Canva as asset |
Automatic — the bot does not call anything.
event-publish.Keep messages simple and non-technical. Share only what the user needs to act on — event links, image options, next steps. Internal IDs, endpoint names, and API details are plumbing the user doesn't care about; omit them unless the user is explicitly debugging.
Translate errors to plain language. Never expose raw JSON, error codes, or webhook URLs.
| HTTP Status | User-Facing Response |
|---|---|
| 400 | Explain which fields are missing or invalid |
| 409 | "There's already an event with that name on that date." |
| 429 | "Let's slow down and try again shortly." |
| 401 | "Something went wrong on my end. Let me look into it." |
If event-publish returns one platform error and one success, tell the user which worked and which failed.