diff --git a/apps/web/components/settings/api-keys.tsx b/apps/web/components/settings/api-keys.tsx
new file mode 100644
index 00000000..3c591372
--- /dev/null
+++ b/apps/web/components/settings/api-keys.tsx
@@ -0,0 +1,672 @@
+"use client"
+
+import { dmSans125ClassName } from "@/lib/fonts"
+import { formatRelativeTime } from "@/components/settings/sync-utils"
+import { cn } from "@lib/utils"
+import { useAuth } from "@lib/auth-context"
+import { authClient } from "@lib/auth"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@ui/components/alert-dialog"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@ui/components/dialog"
+import { Input } from "@ui/components/input"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@ui/components/select"
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
+import { Check, Copy, KeyRound, Loader2, Plus, Trash2 } from "lucide-react"
+import { useCallback, useId, useState } from "react"
+import { toast } from "sonner"
+
+type ListedApiKey = {
+ id: string
+ name: string | null
+ start: string | null
+ key?: string | null
+ createdAt: string
+ expiresAt: string | null
+ lastRequest: string | null
+ enabled: boolean
+ isScoped: boolean
+ containerTags: string[] | null
+ smType: string | null
+ smClient: string | null
+}
+
+const EXPIRY_OPTIONS = [
+ { label: "1 year", value: "365" },
+ { label: "6 months", value: "180" },
+ { label: "30 days", value: "30" },
+ { label: "7 days", value: "7" },
+ { label: "Never", value: "0" },
+] as const
+
+const API_URL =
+ process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
+
+function SettingsCard({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
+function formatKeyPreview(start: string | null | undefined): string {
+ if (!start) return "sm_••••••••"
+ const TYPICAL_TOTAL_LEN = 48
+ const TAIL_STARS_MIN = 8
+ const tailStars = Math.max(TAIL_STARS_MIN, TYPICAL_TOTAL_LEN - start.length)
+ return `${start}${"•".repeat(Math.min(tailStars, 16))}`
+}
+
+function isExpired(expiresAt: string | null): boolean {
+ if (!expiresAt) return false
+ return new Date(expiresAt).getTime() <= Date.now()
+}
+
+function formatExpiresLabel(expiresAt: string | null): string {
+ if (!expiresAt) return "Never"
+ const date = new Date(expiresAt)
+ if (Number.isNaN(date.getTime())) return "—"
+ if (date.getTime() <= Date.now()) return "Expired"
+ return date.toLocaleDateString()
+}
+
+function extractCreatedKey(result: unknown): string {
+ if (!result || typeof result !== "object") {
+ throw new Error("API key missing from response")
+ }
+ const r = result as {
+ key?: string
+ data?: { key?: string }
+ error?: { message?: string }
+ }
+ if (r.error?.message) throw new Error(r.error.message)
+ const key = r.key ?? r.data?.key
+ if (!key) throw new Error("API key missing from response")
+ return key
+}
+
+export default function ApiKeys() {
+ const { org } = useAuth()
+ const queryClient = useQueryClient()
+ const nameId = useId()
+
+ const [createOpen, setCreateOpen] = useState(false)
+ const [keyName, setKeyName] = useState("")
+ const [expiryDays, setExpiryDays] = useState("365")
+ const [createdKey, setCreatedKey] = useState(null)
+ const [copied, setCopied] = useState(false)
+ const [revokeTarget, setRevokeTarget] = useState(null)
+
+ const {
+ data: keys = [],
+ isLoading,
+ isError,
+ refetch,
+ } = useQuery({
+ queryKey: ["api-keys", org?.id, "manage"],
+ queryFn: async () => {
+ if (!org?.id) return []
+ const res = await fetch(`${API_URL}/v3/auth/keys?type=keys`, {
+ credentials: "include",
+ })
+ if (!res.ok) {
+ throw new Error("Failed to load API keys")
+ }
+ const data = (await res.json()) as { keys?: ListedApiKey[] }
+ return data.keys ?? []
+ },
+ enabled: !!org?.id,
+ staleTime: 30 * 1000,
+ })
+
+ const invalidateKeys = useCallback(() => {
+ queryClient.invalidateQueries({ queryKey: ["api-keys", org?.id] })
+ queryClient.invalidateQueries({ queryKey: ["api-keys", org?.id, "manage"] })
+ }, [org?.id, queryClient])
+
+ const createKeyMutation = useMutation({
+ mutationFn: async () => {
+ if (!org?.id) throw new Error("Organization is required")
+ const days = Number(expiryDays)
+ const expiresIn = days > 0 ? days * 24 * 60 * 60 : undefined
+ const name =
+ keyName.trim() || `key-${new Date().toISOString().slice(0, 10)}`
+
+ const res = await authClient.apiKey.create({
+ name,
+ expiresIn,
+ metadata: { organizationId: org.id },
+ prefix: `sm_${org.id}_`,
+ })
+ return extractCreatedKey(res)
+ },
+ onSuccess: (key) => {
+ setCreatedKey(key)
+ setKeyName("")
+ setExpiryDays("365")
+ setCopied(false)
+ invalidateKeys()
+ toast.success("API key created")
+ },
+ onError: (error) => {
+ toast.error("Failed to create API key", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ const revokeKeyMutation = useMutation({
+ mutationFn: async (keyId: string) => {
+ const res = await authClient.apiKey.delete({ keyId })
+ if (res && typeof res === "object" && "error" in res && res.error) {
+ const err = res.error as { message?: string }
+ throw new Error(err.message ?? "Failed to revoke API key")
+ }
+ },
+ onSuccess: () => {
+ setRevokeTarget(null)
+ invalidateKeys()
+ toast.success("API key revoked")
+ },
+ onError: (error) => {
+ toast.error("Failed to revoke API key", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ const handleCopy = async (value: string) => {
+ try {
+ await navigator.clipboard.writeText(value)
+ setCopied(true)
+ toast.success("API key copied to clipboard")
+ setTimeout(() => setCopied(false), 2000)
+ } catch {
+ toast.error("Failed to copy API key")
+ }
+ }
+
+ const resetCreateState = () => {
+ setCreateOpen(false)
+ setCreatedKey(null)
+ setKeyName("")
+ setExpiryDays("365")
+ setCopied(false)
+ }
+
+ const handleCreateOpenChange = (open: boolean) => {
+ if (!open) {
+ resetCreateState()
+ return
+ }
+ setCreateOpen(true)
+ }
+
+ return (
+
+
+
+
+
+ API Keys
+
+
+ Create keys for the Supermemory API, SDKs, and custom
+ integrations. Keys are shown once at creation.
+
+
+
+
+
+
+ {isLoading ? (
+
+
+
+ Loading keys…
+
+
+ ) : isError ? (
+
+
+ Couldn't load API keys.
+
+
+
+ ) : keys.length === 0 ? (
+
+
+
+
+
+
+ No API keys yet
+
+
+ Create your first key to use the API programmatically or
+ connect custom tools.
+
+
+
+
+ ) : (
+
+ {keys.map((key) => {
+ const expired = isExpired(key.expiresAt)
+ const disabled = key.enabled === false || expired
+ return (
+ -
+
+
+
+ {key.name?.trim() || "Unnamed key"}
+
+ {key.isScoped && (
+
+ Scoped
+
+ )}
+ {disabled && (
+
+ {expired ? "Expired" : "Disabled"}
+
+ )}
+
+
+ {formatKeyPreview(key.start ?? key.key)}
+
+
+ Created {formatRelativeTime(key.createdAt)}
+
+ Last used{" "}
+ {key.lastRequest
+ ? formatRelativeTime(key.lastRequest)
+ : "never"}
+
+ Expires {formatExpiresLabel(key.expiresAt)}
+
+
+
+
+ )
+ })}
+
+ )}
+
+
+
+ Need docs?{" "}
+
+ API quickstart
+
+ {" · "}
+
+ Developer console
+
+
+
+
+ {/* Create / reveal dialog */}
+
+
+ {/* Revoke confirmation */}
+
{
+ if (!open && !revokeKeyMutation.isPending) setRevokeTarget(null)
+ }}
+ >
+
+
+
+ Revoke API key?
+
+
+ {revokeTarget?.name?.trim()
+ ? `"${revokeTarget.name}" will stop working immediately.`
+ : "This key will stop working immediately."}{" "}
+ Any apps or scripts still using it will fail. This cannot be
+ undone.
+
+
+
+
+ Cancel
+
+ {
+ e.preventDefault()
+ if (revokeTarget) revokeKeyMutation.mutate(revokeTarget.id)
+ }}
+ className="rounded-full bg-[#C73B1B] text-white hover:bg-[#A83217]"
+ >
+ {revokeKeyMutation.isPending ? (
+
+
+ Revoking…
+
+ ) : (
+ "Revoke key"
+ )}
+
+
+
+
+
+ )
+}
diff --git a/apps/web/components/settings/settings-content.tsx b/apps/web/components/settings/settings-content.tsx
index 7694c03a..9b03bddd 100644
--- a/apps/web/components/settings/settings-content.tsx
+++ b/apps/web/components/settings/settings-content.tsx
@@ -11,6 +11,7 @@ import Billing from "@/components/settings/billing"
import Integrations from "@/components/settings/integrations"
import ConnectionsMCP from "@/components/settings/connections-mcp"
import Support from "@/components/settings/support"
+import ApiKeys from "@/components/settings/api-keys"
import { ErrorBoundary } from "@/components/error-boundary"
import { useRouter } from "next/navigation"
import { useQuery } from "@tanstack/react-query"
@@ -26,6 +27,7 @@ import {
User as UserIcon,
Zap,
HelpCircle,
+ KeyRound,
CreditCard,
ShieldAlert,
ChevronRight,
@@ -49,6 +51,7 @@ import { SettingsOrgSwitcher } from "@/components/settings/settings-org-switcher
export const TABS = [
"account",
"billing",
+ "api-keys",
"integrations",
"connections",
"support",
@@ -75,6 +78,12 @@ const NAV_ITEMS: NavItem[] = [
description: "Plan, usage and payments",
icon: ,
},
+ {
+ id: "api-keys",
+ label: "API Keys",
+ description: "Create and manage API keys",
+ icon: ,
+ },
{
id: "integrations",
label: "Integrations",
@@ -481,6 +490,7 @@ export function SettingsContent({
)}
{activeTab === "billing" && }
+ {activeTab === "api-keys" && }
{activeTab === "integrations" && }
{activeTab === "connections" && }
{activeTab === "support" && }
diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts
index f160e612..5ffda827 100644
--- a/apps/web/lib/analytics.ts
+++ b/apps/web/lib/analytics.ts
@@ -222,7 +222,13 @@ export const analytics = {
// settings / spaces / docs analytics
settingsTabChanged: (props: {
- tab: "account" | "billing" | "integrations" | "connections" | "support"
+ tab:
+ | "account"
+ | "billing"
+ | "api-keys"
+ | "integrations"
+ | "connections"
+ | "support"
}) => safeCapture("settings_tab_changed", props),
spaceCreated: () => safeCapture("space_created"),