+
+
{t('client_id', 'Client ID')}
-
-
- {app.clientId}
- copyToClipboard(app.clientId, 'Client ID')}>
- {t('copy', 'Copy')}
-
+
+
+ {app.clientId}
-
-
-
+
+
{t('client_secret', 'Client Secret')}
-
-
+
+
{plaintextSecret ? (
-
+
{plaintextSecret}
) : (
-
+
{t(
'secret_only_shown_on_creation',
'Secret is only shown on creation or rotation'
)}
)}
- {plaintextSecret && (
-
- copyToClipboard(plaintextSecret, 'Client Secret')
- }
- >
- {t('copy', 'Copy')}
-
- )}
+
+
+ {plaintextSecret && (
+
+ )}
+
+
+ {t('rotate_secret', 'Rotate Secret')}
+
+
+
+ {t('delete_app', 'Delete App')}
+
+
-
-
-
- {t('rotate_secret', 'Rotate Secret')}
-
-
- {t('delete_app', 'Delete App')}
-
-
);
};
diff --git a/apps/frontend/src/components/public-api/public.component.tsx b/apps/frontend/src/components/public-api/public.component.tsx
index 334b28f9..4d26dc8b 100644
--- a/apps/frontend/src/components/public-api/public.component.tsx
+++ b/apps/frontend/src/components/public-api/public.component.tsx
@@ -3,7 +3,6 @@
import { useState, useCallback } from 'react';
import { useSWRConfig } from 'swr';
import { useUser } from '../layout/user.context';
-import { Button } from '@gitroom/react/form/button';
import copy from 'copy-to-clipboard';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { useVariables } from '@gitroom/react/helpers/variable.context';
@@ -13,6 +12,447 @@ import { useDecisionModal } from '@gitroom/frontend/components/layout/new-modal'
import { DeveloperComponent } from '@gitroom/frontend/components/developer/developer.component';
import clsx from 'clsx';
+const mcpClients = [
+ 'Claude Code',
+ 'Cursor',
+ 'VS Code / Copilot',
+ 'Windsurf',
+ 'Amp',
+ 'Codex',
+ 'Gemini CLI',
+ 'Warp',
+] as const;
+
+type McpClient = (typeof mcpClients)[number];
+
+const getMcpConfig = (
+ client: McpClient,
+ method: 'header' | 'path',
+ mcpBase: string,
+ apiKey: string
+): { config: string; hint: string } => {
+ const urlWithKey = `${mcpBase}/mcp/${apiKey}`;
+ const urlBase = `${mcpBase}/mcp`;
+ const bearer = `Bearer ${apiKey}`;
+
+ const json = (obj: object) => JSON.stringify(obj, null, 2);
+
+ if (method === 'path') {
+ switch (client) {
+ case 'Claude Code':
+ return {
+ config: `claude mcp add postiz --transport http "${urlWithKey}"`,
+ hint: 'Run this command in your terminal.',
+ };
+ case 'Cursor':
+ return {
+ config: json({ mcpServers: { postiz: { url: urlWithKey } } }),
+ hint: 'Add to .cursor/mcp.json in your project root.',
+ };
+ case 'VS Code / Copilot':
+ return {
+ config: json({
+ servers: { postiz: { type: 'http', url: urlWithKey } },
+ }),
+ hint: 'Add to .vscode/mcp.json in your project root.',
+ };
+ case 'Windsurf':
+ return {
+ config: json({
+ mcpServers: { postiz: { serverUrl: urlWithKey } },
+ }),
+ hint: 'Add to ~/.codeium/windsurf/mcp_config.json',
+ };
+ case 'Amp':
+ return {
+ config: `amp mcp add postiz ${urlWithKey}`,
+ hint: 'Run this command in your terminal.',
+ };
+ case 'Codex':
+ return {
+ config: `# ~/.codex/config.toml\n\n[mcp_servers.postiz]\nurl = "${urlWithKey}"`,
+ hint: 'Add to ~/.codex/config.toml',
+ };
+ case 'Gemini CLI':
+ return {
+ config: json({ mcpServers: { postiz: { url: urlWithKey } } }),
+ hint: 'Add to ~/.gemini/settings.json',
+ };
+ case 'Warp':
+ return {
+ config: json({ postiz: { url: urlWithKey } }),
+ hint: 'Settings > MCP Servers > + Add, then paste this config.',
+ };
+ }
+ }
+
+ switch (client) {
+ case 'Claude Code':
+ return {
+ config: `claude mcp add postiz \\\n --transport http \\\n --header "Authorization: ${bearer}" \\\n "${urlBase}"`,
+ hint: 'Run this command in your terminal.',
+ };
+ case 'Cursor':
+ return {
+ config: json({
+ mcpServers: {
+ postiz: { url: urlBase, headers: { Authorization: bearer } },
+ },
+ }),
+ hint: 'Add to .cursor/mcp.json in your project root.',
+ };
+ case 'VS Code / Copilot':
+ return {
+ config: json({
+ servers: {
+ postiz: {
+ type: 'http',
+ url: urlBase,
+ headers: { Authorization: bearer },
+ },
+ },
+ }),
+ hint: 'Add to .vscode/mcp.json in your project root.',
+ };
+ case 'Windsurf':
+ return {
+ config: json({
+ mcpServers: {
+ postiz: {
+ serverUrl: urlBase,
+ headers: { Authorization: bearer },
+ },
+ },
+ }),
+ hint: 'Add to ~/.codeium/windsurf/mcp_config.json',
+ };
+ case 'Amp':
+ return {
+ config: json({
+ 'amp.mcpServers': {
+ postiz: { url: urlBase, headers: { Authorization: bearer } },
+ },
+ }),
+ hint: 'Add to your Amp settings.json',
+ };
+ case 'Codex':
+ return {
+ config: `# ~/.codex/config.toml\n\n[mcp_servers.postiz]\nurl = "${urlBase}"\nhttp_headers = { "Authorization" = "${bearer}" }`,
+ hint: 'Add to ~/.codex/config.toml',
+ };
+ case 'Gemini CLI':
+ return {
+ config: json({
+ mcpServers: {
+ postiz: { url: urlBase, headers: { Authorization: bearer } },
+ },
+ }),
+ hint: 'Add to ~/.gemini/settings.json',
+ };
+ case 'Warp':
+ return {
+ config: json({
+ postiz: { url: urlBase, headers: { Authorization: bearer } },
+ }),
+ hint: 'Settings > MCP Servers > + Add, then paste this config.',
+ };
+ }
+};
+
+const CopyButton = ({
+ text,
+ label,
+}: {
+ text: string;
+ label: string;
+}) => {
+ const toaster = useToaster();
+ return (
+
{
+ copy(text);
+ toaster.show(`${label} copied to clipboard`, 'success');
+ }}
+ className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
+ >
+
+
+
+
+ {label}
+
+ );
+};
+
+const McpSection = ({
+ user,
+ mcpBase,
+}: {
+ user: { publicApi: string };
+ mcpBase: string;
+}) => {
+ const t = useT();
+ const [activeClient, setActiveClient] = useState
('Claude Code');
+ const [method, setMethod] = useState<'header' | 'path'>('header');
+ const [revealed, setRevealed] = useState(false);
+
+ const { config, hint } = getMcpConfig(
+ activeClient,
+ method,
+ mcpBase,
+ user.publicApi
+ );
+
+ const maskedConfig = revealed
+ ? config
+ : config.replace(new RegExp(user.publicApi.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '*'.repeat(user.publicApi.length));
+
+ return (
+
+
+
+
+ {t('mcp_client_configuration', 'MCP Client Configuration')}
+
+
+ {t(
+ 'connect_your_mcp_client_to_postiz_to_schedule_your_posts_faster',
+ 'Connect Postiz MCP server to your client (Http streaming) to schedule your posts faster.'
+ )}
+
+
+
+
+
+
+
+ {t('auth_method', 'Authentication')}
+
+
+ {(['header', 'path'] as const).map((m) => (
+ setMethod(m)}
+ >
+ {m === 'header'
+ ? t('authorization_header', 'Authorization Header')
+ : t('api_key_in_url', 'API Key in URL')}
+
+ ))}
+
+
+
+
+ {t('mcp_client', 'Client')}
+
+
+ {mcpClients.map((client) => (
+ setActiveClient(client)}
+ >
+ {client}
+
+ ))}
+
+
+
+
+ {hint}
+
+
+ {maskedConfig}
+
+
+
setRevealed(!revealed)}
+ className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
+ >
+
+ {revealed ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ {revealed ? t('hide', 'Hide') : t('reveal', 'Reveal')}
+
+
+
+
+
+
+ );
+};
+
+const cliSteps = [
+ {
+ label: 'Install the CLI',
+ code: 'npm install -g postiz',
+ },
+ {
+ label: 'Set your API key, copy it to your secret files',
+ code: 'export POSTIZ_API_KEY="{API_KEY}"',
+ },
+ {
+ label: 'Install the Postiz skill for your AI agent',
+ code: 'npx skills add gitroomhq/postiz-agent',
+ },
+] as const;
+
+const CliSection = ({
+ apiKey,
+ backendUrl,
+}: {
+ apiKey: string;
+ backendUrl: string;
+}) => {
+ const t = useT();
+ const toaster = useToaster();
+ const [revealed, setRevealed] = useState(false);
+
+ const steps = cliSteps.map((step) => ({
+ ...step,
+ code: step.code.replace('{API_KEY}', apiKey),
+ }));
+
+ const maskedSteps = steps.map((step) => ({
+ ...step,
+ code: revealed
+ ? step.code
+ : step.code.replace(
+ new RegExp(apiKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
+ '*'.repeat(apiKey.length)
+ ),
+ }));
+
+ return (
+
+
+
+
+ {t('cli_and_skills', 'CLI & AI Skills')}
+
+
+ {t(
+ 'cli_description',
+ 'Use the Postiz CLI to automate posting from your terminal, or install the skill to let your AI agent schedule posts for you.'
+ )}
+
+
+
+
+
+ {maskedSteps.map((step, i) => (
+
+
+ {i + 1}. {step.label}
+
+
+ {step.code}
+
+
+ ))}
+
+
setRevealed(!revealed)}
+ className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
+ >
+
+ {revealed ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ {revealed ? t('hide', 'Hide') : t('reveal', 'Reveal')}
+
+
s.code).join(' && ')}
+ label={t('copy_all', 'Copy All')}
+ />
+
+
+
+ );
+};
+
const PublicApiContent = () => {
const user = useUser();
const { backendUrl, frontEndUrl, mcpUrl } = useVariables();
@@ -21,150 +461,191 @@ const PublicApiContent = () => {
const decision = useDecisionModal();
const { mutate } = useSWRConfig();
const [reveal, setReveal] = useState(false);
- const [reveal2, setReveal2] = useState(false);
- const copyToClipboard = useCallback(() => {
- toaster.show('API Key copied to clipboard', 'success');
- copy(user?.publicApi!);
- }, [user]);
- const copyToClipboard2 = useCallback(() => {
- toaster.show('MCP copied to clipboard', 'success');
- copy(`${mcpUrl || backendUrl}/mcp/` + user?.publicApi);
- }, [user]);
+ const t = useT();
const rotateKey = useCallback(async () => {
const approved = await decision.open({
- title: 'Rotate API Key?',
- description:
- 'This will generate a new API key and invalidate the current one. Any integrations using the old key will stop working.',
- approveLabel: 'Rotate',
- cancelLabel: 'Cancel',
+ title: t('rotate_api_key', 'Rotate API Key?'),
+ description: t(
+ 'rotate_api_key_description',
+ 'This will generate a new API key and invalidate the current one. Any integrations using the old key will stop working.'
+ ),
+ approveLabel: t('rotate', 'Rotate'),
+ cancelLabel: t('cancel', 'Cancel'),
});
if (!approved) return;
await fetch('/user/api-key/rotate', { method: 'POST' });
await mutate('/user/self');
setReveal(false);
- setReveal2(false);
- toaster.show('API Key rotated successfully', 'success');
+ toaster.show(
+ t('api_key_rotated', 'API Key rotated successfully'),
+ 'success'
+ );
}, [decision, fetch, mutate, toaster]);
- const t = useT();
-
if (!user || !user.publicApi) {
return null;
}
+
+ const mcpBase = mcpUrl || backendUrl;
+
return (
-
-
-
{t('public_api', 'Public API')}
-
- {t(
- 'use_postiz_api_to_integrate_with_your_tools',
- 'Use Postiz API to integrate with your tools.'
- )}
-
-
- {t(
- 'read_how_to_use_it_over_the_documentation',
- 'Read how to use it over the documentation.'
- )}
-
-
-
- {t('check_n8n', 'Check out our N8N custom node for Postiz.')}
-
+
+
+ {t(
+ 'api_auth_note_line1',
+ 'Use your API Key to automate your own account.'
+ )}
+
+ {t(
+ 'api_auth_note_line2',
+ 'If you are building a product that schedules posts on behalf of other Postiz users,'
+ )}
+
+ {t(
+ 'api_auth_note_line3',
+ 'create an OAuth App under the "Apps" tab. Your users will authorize your app via OAuth2,'
+ )}
+
+ {t(
+ 'api_auth_note_line4',
+ 'and you will receive a pos_ prefixed token that works with the API, MCP, and CLI — just like an API Key.'
+ )}
+
+
+
+
+
+ {t('api_key', 'API Key')}
+
+
+ {t(
+ 'use_postiz_api_to_integrate_with_your_tools',
+ 'Use Postiz API to integrate with your tools.'
+ )}
+
+
+
-
-
-
+
+
+
{reveal ? (
user.publicApi
) : (
- <>
- {user.publicApi.slice(0, -5)}
- {user.publicApi.slice(-5)}
- >
+
+
+ {user.publicApi.slice(0, -5)}
+
+ {user.publicApi.slice(-5)}
+
)}
-
-
- {!reveal ? (
- setReveal(true)}>
- {t('reveal', 'Reveal')}
-
- ) : (
-
- {t('copy_key', 'Copy Key')}
-
- )}
-
+
-
-
+
+
setReveal(!reveal)}
+ className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
+ >
+
+ {reveal ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ {reveal ? t('hide', 'Hide') : t('reveal', 'Reveal')}
+
+
+
+
+
+
+
{t('rotate_key', 'Rotate Key')}
-
+
+
+ window.open(`${frontEndUrl}/modal/dark/all`, '_blank')
+ }
+ className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
+ >
+
+
+
+
+
+ {t('open_wizard', 'Open Wizard')}
+
-
-
{t('mcp', 'MCP')}
-
- {t(
- 'connect_your_mcp_client_to_postiz_to_schedule_your_posts_faster',
- 'Connect Postiz MCP server to your client (Http streaming) to schedule your posts faster.'
- )}
-
-
-
- {reveal2 ? (
- `${mcpUrl || backendUrl}/mcp/` + user.publicApi
- ) : (
- <>
-
- {(`${mcpUrl || backendUrl}/mcp/` + user.publicApi).slice(0, -5)}
-
-
{(`${mcpUrl || backendUrl}/mcp/` + user.publicApi).slice(-5)}
- >
- )}
-
-
- {!reveal2 ? (
- setReveal2(true)}>
- {t('reveal', 'Reveal')}
-
- ) : (
-
- {t('copy_key', 'Copy Key')}
-
- )}
-
-
-
+
-
-
Building your Postiz payload
-
- Sending a POST request to /posts might feel a bit overwhelming as many
- platforms have different requirements.{'\n'}
- We have created an easy way to build your Postiz payload to schedule
- posts. {'\n'}
- You can use the Postiz wizard, and schedule a post with our UI, after
- you added all your text and settings, the wizard will generate the
- payload for you.{'\n'}
-
-
- window.open(`${frontEndUrl}/modal/dark/all`, '_blank')}>
- Open the payload wizard
-
-
-
+
);
};
@@ -175,31 +656,24 @@ export const PublicComponent = () => {
return (
-
-
setSubTab('api')}
- >
- {t('public_api', 'Public API')}
-
-
setSubTab('developer')}
- >
- {t('apps', 'Apps')}
-
+
+ {(['api', 'developer'] as const).map((tab) => (
+ setSubTab(tab)}
+ >
+ {tab === 'api'
+ ? t('access', 'Access')
+ : t('apps', 'Apps')}
+
+ ))}
{subTab === 'api' &&
}
{subTab === 'developer' &&
}