"use client"; import { useEffect, useState } from "react"; type ApiKeyStatus = { configured: boolean; prefix: string | null; }; type RateLimitStatus = { limit: number; used: number; remaining: number; windowSeconds: number; resetsInSeconds: number; bonus?: number; }; type ExtensionRequest = { id: string; status: string; message: string; requestedAt: string; processedAt: string | null; adminNote: string | null; }; type ExtensionUsage = { used: number; limit: number; remaining: number; requests: ExtensionRequest[]; }; type Props = { initialStatus: ApiKeyStatus; initialRateLimit: RateLimitStatus; initialExtensionUsage: ExtensionUsage; }; export function ApiKeySettings({ initialStatus, initialRateLimit, initialExtensionUsage, }: Props) { const [status, setStatus] = useState(initialStatus); const [rateLimit, setRateLimit] = useState(initialRateLimit); const [extensionUsage, setExtensionUsage] = useState(initialExtensionUsage); const [newKey, setNewKey] = useState(null); const [loading, setLoading] = useState(false); const [requesting, setRequesting] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); useEffect(() => { let active = true; const refresh = () => { fetch("/api/account/api-rate-limit") .then(async (res) => { if (!res.ok) return; const data = await res.json(); if (active) setRateLimit(data); }) .catch(() => {}); }; refresh(); const id = setInterval(refresh, 5000); return () => { active = false; clearInterval(id); }; }, []); async function generateKey() { if ( status.configured && !confirm("This will replace your existing API key. Continue?") ) { return; } setLoading(true); setError(null); setNewKey(null); setSuccess(null); try { const res = await fetch("/api/account/api-key", { method: "POST" }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Failed to generate API key"); setNewKey(data.apiKey); setStatus({ configured: true, prefix: data.prefix }); } catch (err) { setError(err instanceof Error ? err.message : "Failed to generate API key"); } finally { setLoading(false); } } async function revokeKey() { if (!confirm("Revoke your API key? External integrations will stop working.")) return; setLoading(true); setError(null); setSuccess(null); try { const res = await fetch("/api/account/api-key", { method: "DELETE" }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Failed to revoke API key"); setStatus({ configured: false, prefix: null }); setNewKey(null); } catch (err) { setError(err instanceof Error ? err.message : "Failed to revoke API key"); } finally { setLoading(false); } } async function requestRateLimitExtension() { if (extensionUsage.remaining <= 0) return; const reason = prompt( "Optional: tell us why you need a higher API rate limit (leave blank to skip).", ); if (reason === null) return; setRequesting(true); setError(null); setSuccess(null); try { const res = await fetch("/api/account/api-rate-limit-extension-request", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: reason }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Failed to submit request"); setExtensionUsage({ used: data.used, limit: data.limit, remaining: data.remaining, requests: data.requests ?? extensionUsage.requests, }); setSuccess( `Rate limit extension request submitted. ${data.used} of ${data.limit} used this year.`, ); } catch (err) { setError(err instanceof Error ? err.message : "Failed to submit request"); } finally { setRequesting(false); } } const hasPending = extensionUsage.requests.some((r) => r.status === "PENDING"); const usedPct = Math.min(100, Math.round((rateLimit.used / Math.max(1, rateLimit.limit)) * 100)); return (

Use the REST API to upload files and create batch video jobs programmatically. Pro plan only.

API rate limit

Resets in {rateLimit.resetsInSeconds}s

{rateLimit.used} / {rateLimit.limit} requests used this minute {rateLimit.bonus ? ( (includes +{rateLimit.bonus} bonus) ) : null}

= 90 ? "bg-red-500" : usedPct >= 70 ? "bg-amber-500" : "bg-accent" }`} style={{ width: `${usedPct}%` }} />

{rateLimit.remaining} requests remaining · window {rateLimit.windowSeconds}s

Extension requests this year: {extensionUsage.used} / {extensionUsage.limit}

{extensionUsage.requests.length > 0 && (
    {extensionUsage.requests.slice(0, 5).map((req) => (
  • {new Date(req.requestedAt).toLocaleDateString()} · {req.status} {req.adminNote ? ` · ${req.adminNote}` : ""}
  • ))}
)}
{status.configured && status.prefix && (

Active key: {status.prefix}…

)} {newKey && (

Your new API key (copy now)

{newKey}
)} {success && (
{success}
)} {error && (
{error}
)}
{status.configured && ( )}

Endpoints

  • POST /api/v1/upload: upload image or audio file
  • POST /api/v1/jobs: create job from paths (recommended for large batches)
  • POST /api/v1/jobs/batch: small packs only (one-shot multipart)
  • GET /api/v1/playlists: list YouTube playlists
  • POST /api/v1/playlists: create a YouTube playlist
  • GET /api/v1/jobs: list jobs
  • GET /api/v1/jobs/:id: job status

Send Authorization: Bearer YOUR_API_KEY on every request. For many audio files, upload each file then call{" "} /api/v1/jobs (avoid large one-shot batches).{" "} Full API docs

); }