================================================================================ API DOCUMENTATION Generated: 2026-09-16T03:04:20.996Z ================================================================================ BASE URL: http://soundslikemichael.com AUTHENTICATION -------------- Bearer token required in Authorization header: Authorization: Bearer Get token via OAuth flow at: http://soundslikemichael.com/auth CORE ENDPOINTS -------------- Authentication: GET /api/auth/me - Get current user info POST /api/auth/logout - Logout current session POST /api/auth/refresh - Refresh access token Entities: Couple_templates: POST /api/entities/couple_templates - Create new couple_templates Bot_trainings: POST /api/entities/bot_trainings - Create new bot_trainings Email_templates: GET /api/entities/email_templates - List all email_templatess GET /api/entities/email_templates/{id} - Get single email_templates GET /api/entities/email_templates?search= - Full-text search (slug, subject, body_template, from_name, from_email, reply_to, name, status, trigger_event, send_time_aest) Fields: tenant_id, channel, slug, subject, body_template, is_active, from_name, from_email, reply_to, name, status, send_days, trigger_event, sort_order, send_time_aest Thank_you_pages: GET /api/entities/thank_you_pages - List all thank_you_pagess GET /api/entities/thank_you_pages/{id} - Get single thank_you_pages GET /api/entities/thank_you_pages?search= - Full-text search (slug, page_heading, body_copy) Fields: tenant_id, slug, music_type, hour_bucket, is_placeholder, video_url, page_heading, body_copy, is_fallback Magic_links: POST /api/entities/magic_links - Create new magic_links Leads: POST /api/entities/leads - Create new leads Couple_sessions: POST /api/entities/couple_sessions - Create new couple_sessions Couple_files: POST /api/entities/couple_files - Create new couple_files Couple_addons: POST /api/entities/couple_addons - Create new couple_addons Bot_messages: POST /api/entities/bot_messages - Create new bot_messages Couples: POST /api/entities/couples - Create new couples Blog_posts: GET /api/entities/blog_posts - List all blog_postss GET /api/entities/blog_posts/{id} - Get single blog_posts GET /api/entities/blog_posts?search= - Full-text search (title, slug, excerpt, body, cover_image, tags, seo_title, meta_description, head_scripts, cta_button_label, cta_heading, cta_body, cover_focus) Fields: title, slug, excerpt, body, cover_image, tags, published, seo_title, meta_description, head_scripts, published_at, cta_button_label, cta_enabled, cta_heading, cta_body, cover_focus Manual_payments: POST /api/entities/manual_payments - Create new manual_payments Bonjoro_configs: GET /api/entities/bonjoro_configs - List all bonjoro_configss GET /api/entities/bonjoro_configs/{id} - Get single bonjoro_configs GET /api/entities/bonjoro_configs?search= - Full-text search (campaign_name, assignee_name, note_template) Fields: campaign_id, campaign_name, assignee_id, assignee_name, enabled, note_template Contact_messages: POST /api/entities/contact_messages - Create new contact_messages Artists: POST /api/entities/artists - Create new artists Custom Endpoints: POST /api/admin/zoho-invoice-webhook [Public] Inbound Zoho Invoice paid-status webhook. Public route, authenticated by a shared secret (?secret= must equal env.ZOHO_INVOICE_WEBHOOK_SECRET) — 401 otherwise. Parses invoice_id from common Zoho payload shapes, resolves the couple by zoho_invoice_id (deposit) or zoho_balance_invoice_id (balance). Trusts a payload status of 'paid'; if the payload status is ambiguous it re-queries the Zoho Invoice API (AU DC, cached token) to confirm. Marks deposit_status/balance_status='paid' + stamps deposit_paid_at/balance_paid_at. IDEMPOTENT (already-paid → no-op) and FORWARD-ONLY (only ever sets 'paid'). Ignores (200) unmatched invoices or non-paid statuses. Logs [zoho_webhook_paid] to bot_message. STAGE 3 PIECE 2: after newly marking an invoice paid, awaits a fail-soft self-fetch to /api/portal/sync-calendar (shared secret) so the calendar status block reflects 'Paid'. POST /api/portal/verify-magic-link [Public] Couple portal: verifies a magic token (must exist, be unused, and unexpired), marks it single-use (used_at), then mints a 14-day opaque session token in couple_sessions and returns { session_token, couple_id, slug }. POST /api/portal/logout [Public] Couple portal: revokes a session by deleting the couple_sessions row for the given token. Always returns { success:true } (idempotent). GET /api/templates [Public] Public metadata for the SLM run_sheet + agreement templates (exists/filename/size/uploaded_at). Drives the couple-facing download button state. GET /api/templates/download [Public] Public template download. Reads the R2 object for ?type=run_sheet|agreement and returns base64 + filename + content_type for a client-side Blob download. GET /api/templates/agreement [Public] Public agreement PDF streamer. Resolves the SLM agreement/contract couple_template (tolerant match on file_type or filename) and streams its R2 object INLINE (Content-Type from content_type, Content-Disposition: inline) so the Stage 3 agreement-email button opens the real PDF in-browser. couple_template.url is an R2 storage key, not a public URL — this endpoint bridges that gap. 404s with a friendly message when no agreement template is uploaded yet. POST /api/portal/addons [Public] Build C Feature 2 — couple-facing add-on read + balance-invoice self-heal. Public route, session-gated by the opaque couple session token (same pattern as /api/portal/me). couple/couple_session/couple_addon are anonymous_writable → system context. Actions: 'list' → returns the couple's own add-ons ({id,label,amount}) + base_balance (couple.quoted_balance) for the Invoices & Agreement tab display. 'reapply' → SELF-HEAL: rebuilds the Zoho balance invoice IN PLACE as [base balance line] + [add-on lines] so a couple's Core Data pricing edit (which makes save-draft's recompute write a base-only balance PUT) never drops the manual add-on line items. NO-OP when there are no add-ons (save-draft's base-only PUT is already correct), no balance invoice, or the balance is already paid. Fired fire-and-forget by usePortalDraft after every Core Data submit. Fail-soft (logs [stage3_addon_reapplied]/[stage3_error] to bot_message, never throws to the couple). Avoids editing the large save-draft Stage 3 handler. GET /api/bot-training-file [Public] Build E — public inline streamer for a single AI Admin training file, keyed by ?id=. Reads the bot_training row (system context; anonymous_writable) and streams its R2 object inline. Exists so parseDocument(env, url) can fetch an uploaded PDF server-side during the /api/admin/bot-training upload flow (userStorage keys aren't publicly addressable otherwise). Mirrors the existing public /api/templates/* streamers; serves Michael's own general reference content, never couple PII. POST /api/portal/bot-chat [Public] AI Admin (was SLM Bot) — session-gated couple chat assistant. Public route, authenticated by the opaque couple session token (same as /api/portal/me). Actions: 'load' (bot_message history + one-time AI Admin welcome seed) and 'send'. v1.28.0 (Build E): (1) renamed SLM Bot → AI Admin in the welcome + persona + escalation email. (2) DJ-name phrasing from live music_type: DJ-only → "you've booked us" (could be Michael OR Carolin); DJ+Live or Live-only → "you've booked Michael" (live is always Michael). (3) GENERAL-vs-PERSONAL routing: personal = first-person pronoun AND a booking keyword (stricter than a bare pronoun so "how long should my dance floor be?" stays general) — LIVE BOOKING DATA is injected ONLY for personal questions; general questions get KB + training reference only, with no private booking specifics. (4) Personal facts now derived from LIVE couple.core_data (music_start/end time → window + 5h-floored hours, current music_type label, dj_choice, three-state MC via the Michael-fronting rule) — not the stale quiz snapshot. Always injects uploaded bot_training docs (content_text) as ADDITIONAL TRAINING REFERENCE alongside queryRagForContext KB grounding, then runs gemini-3-flash-preview (falls back to gemini-3-pro-preview) via routeToAiProviderWithUsage. Read-only; escalation via [[FLAG_MICHAEL|urgency|summary]] → emails connect@soundslikemichael.com (inlined Zoho Mail adapter, shared-sender fallback) + writes an escalated bot_message; token stripped before the couple sees it. Chat closes 1 week after wedding_date. couple/couple_session/couple_file/bot_message/bot_training are anonymous_writable → system entity context. POST /api/portal/notify-price-change [Public] SLM price-change alert (Aug 2026). Fired fire-and-forget by /api/portal/save-draft's Core Data RE-SUBMIT recompute when a booked couple whose DEPOSIT is already 'paid' and whose balance invoice is NOT yet issued (balance_status not sent/paid/receipt_uploaded AND no zoho_balance_invoice_id) changes something that alters their final price. Nothing auto-corrects in that window (no Zoho balance invoice exists to PUT), so this emails Michael (env SLM_ADMIN_EMAIL || connect@soundslikemichael.com) the couple's details, an itemised list of what changed (e.g. Hours 5 → 6, MC service added), and the old → new total. Shared-secret auth (p.secret === env.SLM_CALENDAR_SYNC_SECRET, mirrors sync-calendar/notify-client-event) so save-draft can call it server-side; 401 otherwise. couple/bot_message are anonymous_writable → system context. Read-only apart from a [price_change_notified] audit log to bot_message. Sends via sendEmail(env) (same admin-notify path as balance-invoice-decision). Never throws to the caller (fail-soft). Returns { success, email_sent, email_error }. GET /api/blog/image [Public] Public blog image streamer, keyed by ?key=blog-images/.. Reads the R2 object via userStorage (blog cover/share images stored by /api/admin/blog-image are R2 keys, not public URLs) and streams the raw bytes INLINE with the correct content-type + long cache headers. Public so anonymous /blog visitors' browsers and JSON-LD image.url references can load cover images. Only serves keys under the blog-images/ prefix (public content, never couple PII). Mirrors /api/templates/run-sheet. POST /api/portal/me [Public] Couple portal: validates an opaque session token and returns the session's couple booking summary + add-ons. 401 { authenticated:false } for missing/expired/invalid session (expired sessions pruned). Identity is session-driven, never the URL slug. Returns deposit/balance state-machine timestamps + parses core_data for music_start_time/music_end_time. v1.23.1 (Issue 3/4): "Your Booking" reflects the couple's CURRENT Core Data (music_type from core_data, hours from the music window w/ 5h floor, three-state mc_service). quoted_total is the source of truth (recomputed by save-draft). Manual Payments (Aug 2026): (1) INITIALISE couple.balance_remaining = quoted_balance + add-ons on load when deposit_status==='paid' AND balance_remaining is NULL (fail-soft, matches total − deposit). (2) Exposes balance_remaining (EFFECTIVE: stored value if set, else fallback quoted_balance + add-ons) + balance_remaining_initialised so the portal's "Balance Remaining" line reads a single source. Read-only apart from the one-time init. POST /api/admin/balance-invoice-decision [Public] SLM Part 5 (Aug 2026) — balance-invoice DECISION + admin-notify. Called by the daily 45-days-out lifecycle cron BEFORE it creates/sends the Zoho balance invoice, so the invoice amount honours manual payments (couple.balance_remaining) and a paid-in-full couple gets NO invoice (admin notified instead). Shared-secret auth (p.secret === env.SLM_CALENDAR_SYNC_SECRET, matching sync-calendar/couple-addons) so a scheduled task can call it; 401 otherwise. requires_auth:false. Money in WHOLE DOLLARS. couple/couple_addon/bot_message anonymous_writable → system context. Body { secret, couple_id | lead_id }. GUARD 1: couple.zoho_balance_invoice_id set → { action:'already_sent' } (never a 2nd invoice). GUARD 2: couple.balance_invoice_decision_at set → { action:'already_decided' } (no re-notify). Decision: balance_remaining NULL → { action:'send', amount = quoted_balance + add-ons } (existing full-balance behaviour); >0 → { action:'send', amount = balance_remaining }; <=0 → { action:'skip' }: sends an admin notification email via sendEmail(env) to env.SLM_ADMIN_EMAIL||connect@soundslikemichael.com ('Balance invoice skipped — (paid in full)'; overpay note when <0), stamps balance_invoice_decision_at to prevent daily re-notify, logs [balance_invoice_skipped]. 'send' path logs [balance_invoice_decision] and returns amount for the cron to invoice (no side effects, guarded next run by zoho_balance_invoice_id the cron sets on success). Pure/idempotent apart from the one-time skip notify+stamp. No client-facing controls. POST /api/portal/request-magic-link [Public] Couple portal: accepts an email, finds the matching couple, mints a 32-char single-use magic token (15-min expiry) in magic_links, and emails the sign-in link via the Zoho Mail adapter (connect@soundslikemichael.com), shared-sender fallback. Enumeration-safe generic success when no couple matches. Aug 2026: greeting uses BOTH partner names when available ("Steph & Alex"), falling back to whichever single first name exists, else "there" — matches the admin "Resend portal sign-in link" button. Aug 2026 (signature): the email now sends as HTML (was plaintext) — styled sign-in button + plaintext fallback link + the Sounds Like Michael signature footer image (env SLM_EMAIL_SIGNATURE_URL, responsive max-width:400px). POST /api/quiz-submit [Public] Quiz submission trigger: resolves the thank-you slug via 3-tier fallback and sends the auto-quote (couple) + admin notification emails via the provider-aware adapter (Zoho Mail when configured, shared sender fallback). Public — couples are anonymous. v1.61.0: ADMIN TOTAL FIX — added {total_price} merge field = base_price + $297 when has_mc_upgrade (else base_price). The admin new-lead notification subject + "Resolved price" now use {total_price} so MC leads report the true total (base + MC), not the MC-exclusive matrix cell. Couple auto-quote still uses {base_price}/{all_in_one_price} UNCHANGED. v1.60.0: QUOTE SUMMARY REWRITE — buildPackageSummary emits full per-tier inclusion lists; MC Service is the TOP inclusion bullet. v1.59.0 link absolutizer, v1.58.0 Bonjoro profile-first auto-fire — UNCHANGED. Note merge fields: {couple_names}{wedding_date}{city}{music_type}{quoted_total}{email}. POST /api/portal/notify-client-event [Public] SLM admin notification (Aug 2026) — emails connect@soundslikemichael.com (env SLM_ADMIN_EMAIL override) whenever a client hits a portal milestone: uploads a payment receipt (invoice_uploaded, detail deposit|balance), uploads a signed agreement (agreement_uploaded), submits their music details (music_submitted), or uploads their run sheet (run_sheet_uploaded). music_submitted includes the actual music selections grouped by section. File-upload events keep the compact Couple/Email/Wedding-date card. Sep 2026: the footer link in this ADMIN email now reads "View their profile →" and deep-links to the admin Clients panel (/admin/clients?lead=) instead of the couple's own portal — this email is sent to Michael, not the couple. Dual auth (couple session_token OR secret+couple_id). couple/couple_session/bot_message are anonymous_writable → system context. Fail-soft. Returns { success, email_sent, email_error, provider }. POST /api/portal/upload-file [Public] Couple portal file list/upload/delete (session-token gated). Sep 7 2026 FIX: run-sheet uploads now PATCH the Google Calendar event DIRECTLY via the Calendar API — the old self-fetch to /api/portal/sync-calendar from inside the Worker always failed (Worker can't reach its own hostname, 530/522, swallowed), so the event never auto-updated after an upload. Also: payment-receipt state machine + admin milestone notify unchanged. POST /api/portal/resend-invoice [Public] Couple portal: re-sends the couple's ISSUED-but-unpaid invoice (deposit first, else balance) via Zoho's invoice-email endpoint, which ATTACHES the actual invoice PDF. Session-token gated (couples have no JWT). Never creates/modifies invoices, never changes amounts. Only resendable when actually issued (deposit_status/balance_status is 'sent'/'receipt_uploaded' AND the matching Zoho invoice id exists). Sep 2026: subject + body render from the editable `email_template` rows (slugs invoice-resend-deposit / invoice-resend-balance) — the render helper now runs markdown→HTML on the template body so admins can store clean markdown (matching the scheduled-email engine) while Zoho still receives proper HTML. Fail-soft falls back to the exact hard-coded copy if a row is missing. Logs [invoice_resend] to bot_message. Returns { success:false } when nothing outstanding is issued. GET /api/email-signature [Public] Self-hosted email signature proxy — streams Michael's signature footer PNG from the app's own domain (soundslikemichael.com) so mail image-proxies fetch it from the app domain instead of the shared assets.overskill.com host. SLM_EMAIL_SIGNATURE_URL points here. POST /api/portal/notify-core-complete [Public] SLM admin notification (Aug 2026): emails connect@soundslikemichael.com (env SLM_ADMIN_EMAIL override) a full copy of a couple's Core Data answers the moment they FIRST complete the Core Details form in the portal. Session-token gated. Fired fire-and-forget by usePortalDraft ONLY on the first Core Data submit. Reads couple.core_data, formats all 13 answers with human labels. Sep 2026: the footer link in this ADMIN email now reads "View their profile →" and deep-links to the admin Clients panel (/admin/clients?lead=) instead of the couple's own portal — this email is sent to Michael, not the couple. Zoho Mail adapter (shared-sender fallback). couple/couple_session/bot_message are anonymous_writable → system entity context. Fail-soft. Returns { success, email_sent, email_error, provider }. POST /api/contact-submit [Public] Public Contact Us form submit. Body { name, email, message, category, company_website (honeypot) }. Silently drops bot submissions when the honeypot is filled. Stores a contact_message row (anonymous_writable) and emails Michael at connect@soundslikemichael.com. Sep 2026: now sends via the Zoho Mail adapter (FROM connect@soundslikemichael.com, into the Zoho outbox — matching every other system notification) with the shared noreply@mail.overskill.app sender kept only as a fallback when Zoho isn't configured/reachable. Returns { success }. POST /api/portal/request-balance-invoice [Public] Couple portal (Aug 2026): a booked couple requests their balance invoice be sent EARLY (before the automatic 45-days-out send). Session-token gated (couples have no OverSkill JWT). Read-only apart from the audit trail: it does NOT create/change/send any Zoho invoice — it (1) emails Michael (env SLM_ADMIN_EMAIL || connect@soundslikemichael.com) a warm-editorial admin notification with the couple's info + amount paid so far + balance owing + the request, (2) APPENDS a timestamped "Client requested balance invoice" line to the couple's lead.notes (the Clients notes section — same field LeadNotes.tsx edits), and (3) logs [balance_invoice_requested] to bot_message. Aug 2026 (Zoho Mail): the admin notification now sends via the confirmed flat-schema Zoho Mail adapter (byte-for-byte from /api/reply-lead + /api/portal/notify-client-event) so it comes FROM connect@soundslikemichael.com on the real domain (far better inbox placement than the shared noreply@mail.overskill.app sender) — shared-sender fallback when Zoho isn't configured. replyTo is the couple so Michael can reply straight to them (replyTo added to the Zoho send path too). Effective balance owing computed exactly like the Clients panel + resend-balance-invoice (quoted_balance + add-ons − manual payments, clamped ≥ 0). Guard: 400 if balance_status==='paid' (nothing to send). couple/couple_session/couple_addon/manual_payment/lead/bot_message are anonymous_writable → system entity context. Fail-soft on the note + log (never blocks the request). Returns { success, email_sent, email_error, note_added, balance_owed, provider }. GET /api/templates/run-sheet [Public] Public run-sheet streamer, keyed by ?couple_id=. Resolves the LATEST couple_file row where kind='run_sheet' (uploaded_at desc), reads its R2 object via userStorage, and streams it. Sep 7 2026 FIX: PDFs open INLINE in-browser; Word/Excel/other formats are served as a DOWNLOAD (attachment) with their real content type — previously every file was forced to inline application/pdf, so a .docx run sheet errored in the browser ("Failed to load PDF document"). Public so it can be embedded as the 'Run Sheet: ' line at the bottom of the Google Calendar event. couple_file is anonymous_writable → system entity context. POST /api/portal/save-draft [Public] Couple portal forms (Core Data + Music Details), session-gated. Actions: load / save (autosave) / submit (stamps submitted_at ONLY on first submit). HARD 5-hour music minimum enforced SERVER-SIDE on every Core Data submit (400), runs before any write/chain. Sep 2026 (required-fields gate): a Core Data SUBMIT now rejects with 400 when ANY of the 13 required fields is empty/blank in the merged blob — this runs BEFORE the 5-hour check and BEFORE any write, so an incomplete Core Data can never be stamped submitted or trigger Stage 3. Autosave (partial single-field patches) is NOT gated. PRICE-MATRIX RECOMPUTE on EVERY Core Data submit — now INCLUDES the flat $140 half-hour surcharge. STAGE 3 (first Core Data submit ONLY): Zoho contact, deposit invoice+email, balance invoice+email when <45 days, agreement welcome email, Google Calendar event create. Sep 4 2026: the calendar run-sheet URL now reads PUBLIC_PORTAL_ORIGIN (www) instead of the retired portal. host. v1.40.0 (skip Stage 3): SKIPS deposit invoice+email when deposit_status==='paid' and agreement email when agreement_status==='submitted'. v1.48.0: SELECT-FIELD BLANK GUARD on Core Data merge. Aug 2026 (signature): agreement email appends SLM signature footer. Aug 2026 (balance guard): the late-booking (<45d) balance invoice now SKIPS when the balance is already settled, already invoiced, or a Zoho balance invoice is already linked. Sep 2026 (editable invoice emails): the DEPOSIT and LATE-BOOKING BALANCE invoice emails render subject + body from editable `email_template` rows (slugs deposit-invoice / balance-invoice-late) — the render helper now runs markdown→HTML on the template body so admins can store clean markdown (matching the scheduled-email engine) while Zoho still receives proper HTML. Fail-soft falls back to the exact hard-coded copy. Sep 2026 (Dinner/Breaks): the Stage 3 calendar description now includes a **DINNER / BREAKS PLAYLIST** block (selection + artists/link), matching sync-calendar. Tokens cached in globalThis.__slmGoogleToken / __slmZohoInvoiceToken / __slmZohoToken. POST /api/portal/sync-calendar [Public] Create or update the couple's Google Calendar event on the shared Weddings calendar. Sep 7 2026 (day-parts): the description now respects the couple's Core Details "what parts of the day" selection — CEREMONY / CANAPÉS / Reception blocks only appear when the couple actually booked music for that part. A Reception-only couple (e.g. DJ-only) no longer gets CEREMONY + CANAPÉS blocks full of "N/A". Same fallback as the Music tab: when NO parts are selected (pre-existing bookings), all sections show. Sep 4 2026: run-sheet URL reads PUBLIC_PORTAL_ORIGIN. Sep 3 2026 REFINEMENT #4: sendUpdates=all only when a NEW attendee is added; creates use all. FIX #4: couple session token third auth path. FIX #3: byte-identical description via buildCalendarDescription(); transparency:"opaque". Dinner/Breaks block included. INTEGRATIONS ------------ GET /api/composio/connections - List Composio connections POST /api/composio/execute - Execute Composio action Body: { action, params, provider, alias? }. `alias` names ONE of the signed-in user's own connected accounts for that provider (e.g. "Work Gmail"); omit it to use their default account. ANALYTICS --------- POST /api/analytics/record - Record analytics event BACKGROUND JOBS --------------- POST /api/backend/queue-job - Queue background job GET /api/backend/job-status/{id} - Get job status AI FEATURES ----------- POST /api/ai/chat - AI chat completion POST /api/ai/object - Generate structured JSON (schema-constrained) POST /api/ai/vision - Image/OCR understanding (base64 image -> text/JSON) POST /api/ai/embedding - Generate embeddings KNOWLEDGE BASE -------------- POST /api/rag/upload - Upload document POST /api/rag/query - Query knowledge base GET /api/rag/documents - List documents DELETE /api/rag/documents/{id} - Delete document -------------------------------------------------------------------------------- For detailed OpenAPI specification: http://soundslikemichael.com/api/openapi.json For interactive docs: http://soundslikemichael.com/api-docs