Referință

Webhook-uri

Când un editor publică, retrage sau șterge o înregistrare, Citron trimite un mesaj scurt și semnat către site-ul dumneavoastră, astfel încât acesta să reconstruiască doar paginile care s-au schimbat.

Configurarea unui webhook#

  1. Adăugați adresaÎn consolă, deschideți Setări → Dezvoltatori → Webhook-uri și adăugați endpointul dumneavoastră, de exemplu https://example.com/api/citron-webhook. Trebuie să fie o adresă publică http(s).
  2. Alegeți evenimentele și colecțiileAlegeți oricare dintre entry.published, entry.unpublished și entry.deleted. Lăsați lista de colecții goală pentru a fi notificat despre toate colecțiile.
  3. Păstrați secretulCopiați secretul generat (whsec_…) în mediul site-ului, de exemplu CITRON_WEBHOOK_SECRET. Cu el este semnată fiecare livrare.

Evenimente#

EvenimentTrimis când
entry.publishedO înregistrare este publicată, inclusiv printr-o publicare programată sau ca versiune nouă a unei înregistrări live.
entry.unpublishedO înregistrare live este scoasă de pe site.
entry.deletedO înregistrare live este ștearsă. Ștergerea unei înregistrări care nu a fost publicată niciodată nu trimite nimic, deoarece nicio pagină nu a afișat-o.

Acestea sunt singurele trei evenimente pe care le primește un webhook. Tipurile noi de evenimente vor trebui activate separat pentru fiecare webhook, astfel încât un endpoint existent nu primește niciodată un mesaj pe care nu l-a cerut.

Cererea#

Fiecare livrare este un POST cu corp JSON și trei antete: content-type: application/json, x-citron-event (numele evenimentului) și x-citron-signature. Citron așteaptă răspunsul dumneavoastră până la 10 secunde și ignoră corpul acestuia.

HTTP
POST /api/citron-webhook HTTP/1.1
content-type: application/json
x-citron-event: entry.published
x-citron-signature: sha256=a8db6ae35854b938a4493cfb12b30f5db059390af0a89e9057a10f19ed14fe99

{"event":"entry.published","organizationId":"cmf2x0r1k0000s6014h3j5k7l","collection":"products","entry":{"id":"cmf3k9x2p0004s601q8w7e2rt","slug":"bio-6"},"at":"2026-09-21T09:30:00.000Z"}
Conținutul mesajului
{
  "event": "entry.published",
  "organizationId": "cmf2x0r1k0000s6014h3j5k7l",
  "collection": "products",
  "entry": { "id": "cmf3k9x2p0004s601q8w7e2rt", "slug": "bio-6" },
  "at": "2026-09-21T09:30:00.000Z"
}
CâmpSemnificație
eventNumele evenimentului, același ca în x-citron-event.
organizationIdId-ul spațiului dumneavoastră de lucru.
collectionSlug-ul colecției, ca în /v1/content/{collection}.
entry{ id, slug } al înregistrării. slug poate fi null.
atMomentul în care a avut loc modificarea (ISO 8601).

Cheile și ordinea lor sunt fixe: semnătura se calculează exact peste acești octeți, așa că structura doar se va extinde, fără a se schimba vreodată.

Verificarea semnăturii#

x-citron-signature este sha256= urmat de HMAC-SHA256 în format hexazecimal al corpului brut al cererii, calculat cu secretul webhook-ului. Calculați-l peste octeții primiți, înainte de orice parsare JSON, și comparați în timp constant. Răspundeți cu 401 dacă nu se potrivește.

Antetul nu conține un marcaj de timp. Câmpul at din mesaj este acoperit de semnătură, deci puteți respinge livrările vechi după vechime; păstrați o marjă generoasă, deoarece livrările așteaptă într-o coadă atunci când procesul de lucru Citron este oprit.

import { constructWebhookEvent, WebhookVerificationError } from '@citron/sdk/webhooks';

// Any runtime with Request/Response: Node 18+, edge, Workers
export async function handleCitronWebhook(req: Request): Promise<Response> {
  const raw = await req.text(); // the RAW body: re-serialised JSON has different bytes
  try {
    const event = await constructWebhookEvent(raw, req.headers.get('x-citron-signature'), process.env.CITRON_WEBHOOK_SECRET!, {
      maxAgeSeconds: 24 * 3600, // optional; the signed "at" makes old deliveries refusable
    });
    console.log(event.event, event.collection, event.entry.slug);
    return new Response(null, { status: 204 });
  } catch (err) {
    if (err instanceof WebhookVerificationError) return new Response(null, { status: 401 });
    throw err;
  }
}

Pentru a testa un handler, semnați singur un corp: signWebhookBody(body, secret) din @citron/sdk/webhooks returnează valoarea antetului. Exemplul de mai sus a fost semnat cu secretul whsec_3Jx9mQ2vR7tLp8Kd4Nw6Yb1Hs5Fc0Zeq.

Revalidarea unui site Next.js#

SDK-ul include un route handler pentru App Router. Etichetați cererile fetch cu citronTags(collection, slug), iar handlerul revalidează exact acele etichete când o înregistrare se schimbă. Paginile rămân statice până când un editor publică.

app/api/citron-webhook/route.ts
// app/api/citron-webhook/route.ts
import { revalidateTag } from 'next/cache';
import { citronTags, createRevalidateHandler } from '@citron/sdk/next';

// 503 without a secret, 401 for a bad signature, 500 if onEvent throws (the API retries)
export const POST = createRevalidateHandler({
  secret: process.env.CITRON_WEBHOOK_SECRET,
  onEvent: (e) => citronTags(e.collection, e.entry.slug).forEach((tag) => revalidateTag(tag)),
});
TypeScript SDK
import { citronTags } from '@citron/sdk/next';

// Tag the fetch so the webhook above can revalidate exactly this page:
// citronTags('products', 'bio-6') → ['citron:products', 'citron:products:bio-6']
const product = await citron.collection('products').find('bio-6', {
  fetchOptions: { next: { revalidate: 3600, tags: citronTags('products', 'bio-6') } },
});
console.log(product?.slug);

Reîncercări#

Orice răspuns 2xx este considerat o livrare reușită. Un timeout, o eroare de rețea sau orice alt cod de stare duce la încă două încercări: după 10 secunde și din nou după un minut. După a treia încercare, livrarea este abandonată. O adresă care se rezolvă într-o rețea privată este refuzată și nu se reîncearcă.

  • Răspundeți rapid și faceți munca lentă ulterior; un handler care durează mai mult de 10 secunde este reîncercat chiar dacă a terminat.
  • Faceți handlerul idempotent. Revalidarea de două ori a unei etichete este inofensivă.
  • Returnați 500 când propria prelucrare eșuează și doriți o nouă încercare; handlerul din SDK face acest lucru când onEvent aruncă o eroare.

Această referință acoperă doar API-ul public și respectă contractul de compatibilitate al API-ului: răspunsurile publice pot doar să primească câmpuri noi. Descrierea OpenAPI a acestor rute se află la /developers/openapi.json.