From fa181f0155468861915de7c09507d23c61727efc Mon Sep 17 00:00:00 2001
From: Waleed
- These events in {name} can automatically start a Sim workflow — no polling - required. +
+ Connect a {name} webhook to Sim and your workflow fires the instant an event + happens — no polling, no delay. Sim receives the full event payload and makes + every field available as a variable inside your workflow.
+ + {/* Event cards */}{trigger.name}
@@ -462,73 +470,6 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl )} - {/* Popular workflows featuring this integration */} - {featuredPairs.length > 0 && ( -- Common automation patterns teams build on Sim using {name}. -
-{headline}
-{desc}
-+
{template.title}
- +Try this workflow → -
+ ) @@ -660,40 +616,34 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl- Full API reference, authentication setup, and usage examples for {name}. -
- - docs.sim.ai - - + Get started free + + + View docs + + +
+ + Request submitted — we'll follow up at{' '} + {email}. +
+Last Updated: October 11, 2025
diff --git a/apps/sim/app/(landing)/terms/page.tsx b/apps/sim/app/(landing)/terms/page.tsx
index 61805f461dc..e5c6099649e 100644
--- a/apps/sim/app/(landing)/terms/page.tsx
+++ b/apps/sim/app/(landing)/terms/page.tsx
@@ -1,19 +1,11 @@
-'use client'
-
-import { useEffect } from 'react'
import Link from 'next/link'
import { getEnv } from '@/lib/core/config/env'
-import { LegalLayout } from '@/app/(landing)/components'
+import { ExternalRedirect, LegalLayout } from '@/app/(landing)/components'
export default function TermsOfService() {
- useEffect(() => {
- const termsUrl = getEnv('NEXT_PUBLIC_TERMS_URL')
- if (termsUrl?.startsWith('http')) {
- window.location.href = termsUrl
- }
- }, [])
return (
Last Updated: October 11, 2025
diff --git a/apps/sim/app/api/help/integration-request/route.ts b/apps/sim/app/api/help/integration-request/route.ts
new file mode 100644
index 00000000000..c6773d2f686
--- /dev/null
+++ b/apps/sim/app/api/help/integration-request/route.ts
@@ -0,0 +1,110 @@
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { env } from '@/lib/core/config/env'
+import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
+import { RateLimiter } from '@/lib/core/rate-limiter'
+import { generateRequestId } from '@/lib/core/utils/request'
+import { getEmailDomain } from '@/lib/core/utils/urls'
+import { sendEmail } from '@/lib/messaging/email/mailer'
+import { getFromEmailAddress } from '@/lib/messaging/email/utils'
+
+const logger = createLogger('IntegrationRequestAPI')
+
+const rateLimiter = new RateLimiter()
+
+const PUBLIC_ENDPOINT_RATE_LIMIT: TokenBucketConfig = {
+ maxTokens: 10,
+ refillRate: 5,
+ refillIntervalMs: 60_000,
+}
+
+const integrationRequestSchema = z.object({
+ integrationName: z.string().min(1, 'Integration name is required').max(200),
+ email: z.string().email('A valid email is required'),
+ useCase: z.string().max(2000).optional(),
+})
+
+export async function POST(req: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
+ const storageKey = `public:integration-request:${ip}`
+
+ const { allowed, remaining, resetAt } = await rateLimiter.checkRateLimitDirect(
+ storageKey,
+ PUBLIC_ENDPOINT_RATE_LIMIT
+ )
+
+ if (!allowed) {
+ logger.warn(`[${requestId}] Rate limit exceeded for IP ${ip}`, { remaining, resetAt })
+ return NextResponse.json(
+ { error: 'Too many requests. Please try again later.' },
+ {
+ status: 429,
+ headers: { 'Retry-After': String(Math.ceil((resetAt.getTime() - Date.now()) / 1000)) },
+ }
+ )
+ }
+
+ const body = await req.json()
+
+ const validationResult = integrationRequestSchema.safeParse(body)
+ if (!validationResult.success) {
+ logger.warn(`[${requestId}] Invalid integration request data`, {
+ errors: validationResult.error.format(),
+ })
+ return NextResponse.json(
+ { error: 'Invalid request data', details: validationResult.error.format() },
+ { status: 400 }
+ )
+ }
+
+ const { integrationName, email, useCase } = validationResult.data
+
+ logger.info(`[${requestId}] Processing integration request`, {
+ integrationName,
+ email: `${email.substring(0, 3)}***`,
+ })
+
+ const emailText = `Integration: ${integrationName}
+From: ${email}
+Submitted: ${new Date().toISOString()}
+
+${useCase ? `Use Case:\n${useCase}` : 'No use case provided.'}
+`
+
+ const emailResult = await sendEmail({
+ to: [`help@${env.EMAIL_DOMAIN || getEmailDomain()}`],
+ subject: `[INTEGRATION REQUEST] ${integrationName}`,
+ text: emailText,
+ from: getFromEmailAddress(),
+ replyTo: email,
+ emailType: 'transactional',
+ })
+
+ if (!emailResult.success) {
+ logger.error(`[${requestId}] Error sending integration request email`, emailResult.message)
+ return NextResponse.json({ error: 'Failed to send request' }, { status: 500 })
+ }
+
+ logger.info(`[${requestId}] Integration request email sent successfully`)
+
+ return NextResponse.json(
+ { success: true, message: 'Integration request submitted successfully' },
+ { status: 200 }
+ )
+ } catch (error) {
+ if (error instanceof Error && error.message.includes('not configured')) {
+ logger.error(`[${requestId}] Email service configuration error`, error)
+ return NextResponse.json(
+ { error: 'Email service is temporarily unavailable. Please try again later.' },
+ { status: 500 }
+ )
+ }
+
+ logger.error(`[${requestId}] Error processing integration request`, error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
diff --git a/apps/sim/app/changelog/layout.tsx b/apps/sim/app/changelog/layout.tsx
index e7d7153dfca..5fbd2896730 100644
--- a/apps/sim/app/changelog/layout.tsx
+++ b/apps/sim/app/changelog/layout.tsx
@@ -1,14 +1,16 @@
+import { getNavBlogPosts } from '@/lib/blog/registry'
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import Footer from '@/app/(home)/components/footer/footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
-export default function ChangelogLayout({ children }: { children: React.ReactNode }) {
+export default async function ChangelogLayout({ children }: { children: React.ReactNode }) {
+ const blogPosts = await getNavBlogPosts()
return (
ES@?d|(dk!Rzc)zs^!=ar`u?zO
zPI5}Mt?}y*-&+!GEwm{4eDBYHilL_e&ppYWp8`Ls_+xQ-yII4z{MK>W9yNn8EB`dB
z=mUIJ63)JXzkvI%@?3aDmV1p{{Oxb$q9aCxfBA64Yve8 EiFd}A_K$&HSz`4n^K(R n|{f;Apef@UM*&!c}+6LuO`jC{^8{?-U6wb8EGXx|?r!
zL%D1F8a_Q7;q1`rT&^Agkg)!eT-JGHSr5hE2x5l$J~Xy*qO&%bnW>j}d7eMv-cP=y
zh9nVX{VZwgS~~~Do0n`wegBl#L>@u%+%a2t8#4v-xF0gOZBEkliG+3d?R^zTTrzuV8H81}KyN1$XFR~=5{AkpdVyF`r^za*
z-<}eb1J~o5P_xJKosnb;Ba_`{!xQU)^)pWZY!{B|N_hn!X2^IOKh$Eb)gnG=EGpZOc9PenRlU&~Z1
zDJS=RiMhh#9ln?g;Ok7UQtBZ4MZa-gW6FhX2?p{pN@0NkYu^n8Ugu62qH9{WnmJF8
zxXa#}cB*DO%fXrUhKRo|`GpLX)JI>vKk(xG#ZCBF=%=faQP)}xX@OT|Yno0cUzTp_
ze3FUzr_3>X2ia3e!A1#bLW0{!x~aa-(zgm<-x|`Eb@y+BcdqGpU)=YlhG?m)nTowb
z3A$G3u(*FgQeiWt*^e(tT-l2iyT&g6IRhjL|Da~cXnK!)6v#xiu*>YYIc~rImN{M<
zMB|!h-bC0jM4cFT)aE+`(=>0Xu^51oPbPHhG4`$t6i1iu(WAeJuZHJ-Xp*0(M^-Ov
z7bmkWFksD;9eJymG$A9;ww~vd)cMXy1KLWYaW&IJ`t8oP;KA?2=nArL)J(U&QNAOw
zX3AEC*~#nKl-gM-Rkt7&SYKDVT6W(v{hPX=!xM6_YSf4JHA5veYg=kASg58xL*LhQ
z