// A reference settings page for an app built on Outlevo: the workspace's webhooks. List them, add one (the signing // secret is shown once), send a test delivery, rotate the secret, read recent deliveries and remove one. // Plain fetch through your own server route (/api/outlevo, see allow.ts), so no package is needed and no key // reaches the browser. Show it to your admins only. API keys are created at app.outlevo.com → Developers. import { useCallback, useEffect, useState, type FormEvent } from 'react'; const BASE = '/api/outlevo'; type Webhook = { id: string; url: string; events: string[]; status: string; created_at: string }; type Delivery = { id: string; event_id: string; type: string; attempt: number; status: string; error: string | null; created_at: string }; type ApiError = { error?: { code?: string; message?: string; fix?: string } }; async function call(method: string, path: string, body?: unknown): Promise { // POST bodies go through as JSON; the proxy adds the key and an Idempotency-Key const r = await fetch(`${BASE}${path}`, { method, headers: body === undefined ? undefined : { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) }); const data = (await r.json().catch(() => ({}))) as T & ApiError; if (!r.ok) throw new Error([data.error?.message ?? `HTTP ${r.status}`, data.error?.fix].filter(Boolean).join(' ')); return data; } // The events most apps start with; the full list is at https://developers.outlevo.com/webhooks.html const SUGGESTED = ['lead.replied', 'lead.answered', 'meeting.booked', 'company.qualified', 'handoff.created', 'job.completed', 'job.failed']; function Secret({ value, onClose }: { value: string; onClose: () => void }) { return (

Copy this signing secret now and keep it on your server as OUTLEVO_WEBHOOK_SECRET. It is not shown again.

{value}

); } function Deliveries({ id }: { id: string }) { const [rows, setRows] = useState(null); useEffect(() => { call<{ data: Delivery[] }>('GET', `/v1/webhooks/${id}/deliveries?limit=10`).then((d) => setRows(d.data)).catch(() => setRows([])); }, [id]); if (!rows) return

Loading deliveries…

; if (!rows.length) return

No deliveries yet.

; return ( {rows.map((d) => )}
WhenEventAttemptStatusError
{new Date(d.created_at).toLocaleString()}{d.type}{d.attempt}{d.status}{d.error ?? '–'}
); } export function Settings() { const [hooks, setHooks] = useState(null); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [secret, setSecret] = useState(null); const [open, setOpen] = useState(null); const [url, setUrl] = useState(''); const [events, setEvents] = useState(['lead.replied', 'meeting.booked']); const load = useCallback(() => call<{ data: Webhook[] }>('GET', '/v1/webhooks').then((d) => setHooks(d.data)).catch((e) => setError(String(e.message ?? e))), []); useEffect(() => { void load(); }, [load]); const run = async (fn: () => Promise) => { setError(null); setNotice(null); try { await fn(); await load(); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } }; const add = (e: FormEvent) => { e.preventDefault(); void run(async () => { const created = await call<{ id: string; secret: string }>('POST', '/v1/webhooks', { url, events }); setSecret(created.secret); setUrl(''); }); }; const test = (id: string) => run(async () => { const r = await call<{ delivered: boolean; status_code: number | null; error?: string }>('POST', `/v1/webhooks/${id}/test`); setNotice(r.delivered ? `Test delivered: your endpoint answered ${r.status_code}.` : `Test not delivered${r.status_code ? `: your endpoint answered ${r.status_code}` : ''}${r.error ? ` (${r.error})` : ''}.`); }); const rotate = (id: string) => window.confirm('Issue a new signing secret? The old one keeps working for 24 hours.') && run(async () => { setSecret((await call<{ secret: string }>('POST', `/v1/webhooks/${id}/rotate-secret`)).secret); }); const remove = (id: string) => window.confirm('Stop deliveries to this address?') && run(async () => { await call('DELETE', `/v1/webhooks/${id}`); }); return (

Settings

Webhooks

Outlevo sends signed events to these addresses. Verify each delivery with the signing secret before trusting it.

{error &&

{error}

} {notice &&

{notice}

} {secret && setSecret(null)} />} {!hooks ?

Loading…

: !hooks.length ?

No webhooks yet.

: ( {hooks.map((h) => ( ))}
AddressEventsStatus
{h.url}{h.events.join(', ')}{h.status} {h.status === 'active' && <> {' '} {' '} {' '} }{open === h.id && }
)}

Add a webhook

Events {SUGGESTED.map((ev) => )}

API keys

Keys are created and revoked at app.outlevo.com → Developers. Keep them in your server's environment, never in this page.

); } export default Settings;