Jak reagovat na eventy sběru podkladů z vlastního kódu
Ve chvíli, kdy klient dokončí sběr podkladů, soubory, texty i přístupy, které jste chtěli, už existují — typované, validované, připravené k použití. Otázka je, jak se to dozví váš kód. Tenhle návod je pro vývojáře, kteří chtějí webhook přijímat a rovnou na něj reagovat, místo aby event routovali přes iPaaS jako Zapier nebo n8n. Předpokládá, že už víte, co jsou webhooky a jak se podepisují — ta stránka je referenční, tahle je návod na použití.
Postavíme si receiver, který podpis ověří, podle eventu rozhodne, co udělat, a jakmile je sběr podkladů hotový, vyzvedne typované výsledky — stačí to na spuštění buildu, uložení souborů do repa nebo bucketu, zápis do vlastního dashboardu nebo předání výsledků coding agentovi.
Příklady používají Express, protože ten používají i vlastní quickstart ukázky BriefGate, ale logika se stejně přenese do Fastify, Hono nebo obyčejného Node http serveru — jediná část specifická pro Express je získání raw body.
Registrace endpointu
Zaregistrujte receiver jen na eventy, na které skutečně chcete reagovat — pokud potřebujete vědět jen to, kdy je materiál hotový, item.submitted možná nepotřebujete:
curl -X POST https://api.briefgate.dev/v1/webhooks \
-H "Authorization: Bearer bg_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/briefgate",
"events": ["intake.completed", "intake.stalled", "chase.bounced"]
}'Odpověď obsahuje secret, který se zobrazí právě jednou — uložte si ho jako BRIEFGATE_WEBHOOK_SECRET nebo podobně; znovu ho získat nejde, jen endpoint smazat a zaregistrovat nový. Kompletní tvar požadavku/odpovědi a formát pro Slack/Discord chat najdete v referenci k webhookům; tenhle návod se drží jen formátu raw, tedy toho určeného pro kód.
Jak správně získat raw body
Podpis se počítá přes přesné bajty, které BriefGate odeslal, ne přes váš zparsovaný objekt — pokud nejdřív proběhne JSON parser a vy pak objekt znovu serializujete kvůli ověření podpisu, rozdíly v mezerách nebo pořadí klíčů způsobí, že legitimní požadavek neprojde. Surový řetězec si zachyťte ještě před parsováním:
import express from 'express';
const app = express();
// Stash the raw bytes for verification; req.body still parses normally.
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8'); },
}));Ověření podpisu
BriefGate podepisuje každé doručení hlavičkou X-BriefGate-Signature: t=<unix>,v1=<hex>, kde v1 je HMAC-SHA256(webhook_secret, "${t}.${rawBody}") v hexu. Odmítejte vše starší než 5 minut, ať se brání replay útoku, a v1 porovnávejte přes timingSafeEqual, nikdy ne === — obyčejné porovnání řetězců prozrazuje časováním, kolik počátečních znaků sedělo, a přesně tohle by ověření podpisu dělat nemělo:
import { createHmac, timingSafeEqual } from 'crypto';
function verifyBriefGateSignature(rawBody, secret, signatureHeader) {
if (!signatureHeader) throw new Error('Missing signature header');
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('=', 2)),
);
const t = parts['t'];
const v1 = parts['v1'];
if (!t || !v1) throw new Error('Malformed signature header');
// 5-minute tolerance window, same as BriefGate enforces on its own side.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) {
throw new Error('Timestamp outside tolerance window');
}
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(v1, 'hex');
const b = Buffer.from(expected, 'hex');
// timingSafeEqual throws on a length mismatch, so check that explicitly first.
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('Signature mismatch');
}
}Jedné zkratce se vyhněte: přepodepsání zparsovaného req.body přes JSON.stringify(req.body) místo raw řetězce. Pořadí klíčů a mezery nejsou zaručeně stejné jako to, co BriefGate skutečně odeslal, takže tohle bude občas odmítat platné požadavky — řešením je verify callback výše, který zachytí přesné bajty ještě předtím, než se jich Express dotkne.
Zapojení routy
app.post('/webhooks/briefgate', async (req, res) => {
try {
verifyBriefGateSignature(req.rawBody, process.env.BRIEFGATE_WEBHOOK_SECRET, req.header('X-BriefGate-Signature'));
} catch (err) {
return res.status(400).send('invalid signature'); // discard without acting on the payload
}
const event = req.header('X-BriefGate-Event'); // same as req.body.event, but available pre-parse
const { intake_id } = req.body;
res.status(200).send('ok'); // acknowledge before doing any real work
handleEvent(event, req.body, intake_id).catch(err => {
console.error('webhook handler failed', { event, intake_id, err });
});
});Na tom, že odpovíte dřív, než handleEvent doběhne, záleží: na doručení je rozpočet 10 sekund a sestavování souborů nebo volání dalšího API snadno trvá déle. Náročnou práci udělejte až po odpovědi, nebo ji předejte frontě.
Na které eventy reagovat a jak
Kompletní seznam eventů a tvar obsahu je v referenci k webhookům; tady je, k čemu je každý dobrý v pipeline řízené kódem:
intake.completed— obvykle nejdůležitější. Všechny povinné položky jsou odeslané; zavolejteget_intake_resultsa spusťte, co výsledky odemykají.item.submitted— spouští se za každou položku, ještě před dokončením sběru. Hodí se, pokud chcete začít zpracovávat jeden podklad hned, ale u poslední položky může dorazit předintake.completedi po ní — nespoléhejte na pořadí.intake.stalled— vyčerpal se počet upomínek a BriefGate přestal chasovat. Signál pro člověka, ne pro retry smyčku.intake.overdue— spustí se jednou, když termín uplynul a něco pořád chybí. Hodí se jako příznak v dashboardu.chase.bounced— upomínka natvrdo odrazila nebo byla nahlášená jako spam.still_chasingříká, jestli BriefGate na příjemce nebo sběr podkladů úplně rezignoval.client.viewed/intake.archived— eventy s nižší výpovědní hodnotou; většina pipeline je jen zaloguje.
async function handleEvent(event, payload, intakeId) {
switch (event) {
case 'intake.completed':
await fetchResultsAndKickOffBuild(intakeId);
break;
case 'intake.stalled':
await notifyOwnerToFollowUpManually(intakeId, payload.missing_items);
break;
case 'chase.bounced':
if (!payload.still_chasing) await flagIntakeForManualOutreach(intakeId);
break;
default:
console.log('unhandled event', event, intakeId); // client.viewed, intake.archived, etc.
}
}Idempotence a opakování pokusů
BriefGate opakuje doručení při čemkoli jiném než 2xx, s odstupy rostoucími zhruba přes celý den, a ve výjimečných případech může doručit tentýž event dvakrát i po 2xx. Handler by měl zvládnout běžet dvakrát bez škody — klíčujte podle intake_id + event a práci, která už proběhla, přeskočte:
const seen = new Set(); // swap for Redis/DB in anything beyond a single process
async function fetchResultsAndKickOffBuild(intakeId) {
const dedupeKey = `intake.completed:${intakeId}`;
if (seen.has(dedupeKey)) return;
seen.add(dedupeKey);
const results = await getIntakeResults(intakeId);
// ... start the build, write files, whatever comes next
}Cokoli jiného než 2xx — včetně pádu ještě před odpovědí — se počítá jako neúspěšný pokus a opakuje podle plánu v referenčním dokumentu. Vrácením 410 Gone endpoint záměrně deaktivujete — je to zdokumentovaný způsob odhlášení bez zvláštního API volání.
Vyzvednutí typovaných výsledků
Jakmile máte intake_id z intake.completed, vyzvedněte si výsledky — samotný event schválně nese jen intake_id, project_name a timestamp, ne odpovědi samotné.
async function getIntakeResults(intakeId) {
const url = new URL(`https://api.briefgate.dev/v1/intakes/${intakeId}/results`);
// A secret item can only be revealed once — don't burn it in a pipeline that logs everything.
url.searchParams.set('exclude_secrets', 'true');
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.BRIEFGATE_API_KEY}` },
});
if (!res.ok) throw new Error(`results fetch failed: ${res.status}`);
return res.json();
}GET /v1/intakes/:id/results je jen pro API klíč — bez session dashboardu — protože zároveň posouvá kurzor only_new a umí odhalit secrets, což obojí dává smysl jen pro službu jednající pod vlastním klíčem. Kompletní tvar odpovědi a parametry only_new/include_pending najdete v referenci REST API.
Pokud přístupový údaj skutečně potřebujete — třeba abyste se přihlásili do klientovy instalace WordPressu a nasadili plugin — vynechte exclude_secrets a čtěte se scope secrets:read. Zacházejte s ním jako s jednorázovou hodnotou, čím je: přečtěte, použijte, nezapisujte do vlastních logů v plaintextu. Položky typu secret se šifrují libsodium sealed boxem při příchodu na server BriefGate, nikdy u klienta — co to chrání a co ne, popisuje reference k trezoru na secrets.
Předání výsledků coding agentovi
Pokud je dalším krokem agent, ne skript, nemusíte obsah webhooku ručně přesouvat do promptu. MCP agent připojený k BriefGate umí zavolat get_intake_results sám, jakmile zná intake_id — úkolem vašeho receiveru se stává „všimni si intake.completed a agenta vzbuď", ne „vytáhni a přeformátuj data". Viz Podklady od klienta do coding agenta.
Na co dát pozor: jeden endpoint dostává všechny eventy na účtu, napříč všemi sběry podkladů — pokud přes jeden účet BriefGate vedete víc projektů, filtrujte podle intake_id uvnitř handleru.
Související
- Reference k webhookům — kompletní seznam eventů, tvary obsahu, log doručení, opakování pokusů, formát pro Slack/Discord
- Reference REST API — všechny endpointy, včetně
GET /v1/intakes/:id/results - Trezor na secrets — jak se položky typu secret šifrují a odhalují
- Podklady od klienta do coding agenta — cesta přes MCP, pokud se budí agent