Przejdź do treści

Własny serwis (Custom HTTP)

Dla kogo jest ta integracja

Ta integracja jest dla zespołów, które mają własny CMS, własną stronę albo własny proces publikacji — i chcą, żeby gotowe posty ze Scriberry trafiały dokładnie tam, gdzie potrzeba. Wybierają ją między innymi firmy, które:

  • mają CMS zbudowany na zamówienie i nie używają WordPressa,
  • prowadzą bloga na generatorze stron statycznych (Hugo, Astro, Eleventy) i chcą, żeby nowe posty same lądowały w repozytorium na Git-cie,
  • chcą, żeby zatwierdzone posty trafiały najpierw na Slacka albo do Notion do dodatkowej akceptacji,
  • mają własny pipeline publikacyjny i traktują Scriberry jako jedno z jego źródeł.

W praktyce ta integracja wymaga osoby z zapleczem programistycznym po Twojej stronie — kogoś, kto przygotuje krótki kawałek kodu odbierający posty z naszego serwera. Dalsza część strony jest napisana właśnie dla niej.

Jak to działa (bez technikaliów)

  1. Twój programista przygotowuje na Waszej infrastrukturze adres internetowy (URL), pod który Scriberry będzie wysyłać posty.
  2. Ty w panelu Scriberry tworzysz integrację, podajesz ten adres i wybierasz format treści (HTML lub Markdown).
  3. Scriberry pokazuje Ci jednorazowo sekret podpisujący — przekazujesz go programiście, żeby Wasz serwer mógł sprawdzić, że żądanie naprawdę pochodzi od nas.
  4. Od tej chwili każdy zatwierdzony post w terminie publikacji jest automatycznie wysyłany na podany adres. Wasz serwer go odbiera i robi z nim, co chcecie — zapisuje w bazie, commituje do repo, publikuje, wysyła powiadomienie.

Możesz też w dowolnej chwili wysłać posta testowego z menu integracji, żeby sprawdzić, czy wszystko po Waszej stronie działa.

Utworzenie integracji w Scriberry

  1. Otwórz projekt → zakładka Integracje → kliknij kafelek Własny serwis (Custom HTTP).

Custom request integration

  1. Wypełnij kreator:
  2. Nazwa — cokolwiek, byś rozpoznał ją później.
  3. Adres URL — musi zaczynać się od https://.
  4. Format treściHTML (domyślnie; Scriberry zamienia Markdown na HTML za Was) albo Markdown (oryginał, z obrazami przepisanymi na publiczne adresy CDN).

Custom request first step

  1. Kliknij Utwórz. Scriberry wygeneruje sekret podpisujący i pokaże go razem z adresem (jako potwierdzenie).

Custom request summary step

Sekret pokazujemy tylko raz

Po zamknięciu tego ekranu nie pokażemy go ponownie. Skopiuj go teraz i przekaż programiście w bezpieczny sposób (menedżer haseł, szyfrowana wiadomość). Jeśli go zgubisz, użyj opcji Wygeneruj nowe dane logowania na karcie integracji — stary sekret natychmiast przestanie działać.

Integracja od razu jest Aktywna — w odróżnieniu od WordPressa nie musi się z nami parować. Pierwsze żądanie wyjdzie, gdy tylko nadejdzie termin publikacji pierwszego zatwierdzonego posta.


Część techniczna — dla programisty

Wszystko, co jest poniżej, jest skierowane do osoby, która będzie pisać i utrzymywać endpoint odbierający posty.

Wymagania endpointu

Endpoint, który Scriberry będzie wywoływać, musi:

  • być dostępny po HTTPS (czysty HTTP jest odrzucany w kreatorze),
  • akceptować POST z Content-Type: application/json,
  • zwracać kod 2xx przy sukcesie — każda inna odpowiedź uruchomi ponowienie z rosnącym odstępem czasu,
  • odpowiadać w ciągu kilku sekund — zbyt wolny endpoint może zostać uznany za niedostępny i ponawiamy żądanie.

Nagłówki żądania

Nagłówek Opis
Content-Type Zawsze application/json.
X-Scriberry-Signature sha256=<hex> — HMAC SHA-256 z body żądania, podpisany sekretem.
X-Scriberry-Timestamp Moment podpisania, w sekundach unix-owych. Użyj do ochrony przed replay-em.
X-Scriberry-Event post.publish przy realnych publikacjach i przy wywołaniach z przycisku „Testuj".

Body — kanoniczny payload

{
  "schema_version": "1.0",
  "post": {
    "id": "0193f8c3-...",
    "title": "Dlaczego time-boxing bije listy todo",
    "slug": "time-boxing-bije-listy-todo",
    "excerpt": "Krótkie streszczenie pod <meta name=\"description\">.",
    "content_html": "<p>Pełna treść…</p>",
    "content_format": "html",
    "language": "pl",
    "scheduled_for": "2026-06-01T09:00:00.000Z"
  },
  "target": null,
  "media": {
    "featured": {
      "ref": "01940ab2-...",
      "url": "https://cdn.scriberry.ai/posts/0193f8c3.../cover.webp",
      "alt": "Kalendarz z trzema blokami czasowymi zaznaczonymi na czerwono",
      "mime": "image/webp"
    },
    "inline": []
  },
  "idempotency_key": "0193f8c3-..._v1"
}

Pole target jest zawsze null dla Custom HTTP — wskazówki specyficzne dla WordPressa nie są tu istotne. Pole idempotency_key jest stabilne między ponowieniami — zapisz je po swojej stronie i pomiń żądanie, jeśli zobaczysz ten sam klucz drugi raz.

Jeżeli w kreatorze wybrałeś Markdown, pole content_html zostanie zastąpione przez content_markdown, a content_format ustawione na "markdown".

Weryfikacja podpisu

Sygnatura jest obliczana jako:

HMAC-SHA256(secret, "<X-Scriberry-Timestamp>.<surowe-body-żądania>")

Po stronie endpointu:

  1. Zdejmij prefiks sha256= z nagłówka X-Scriberry-Signature.
  2. Sprawdź, czy X-Scriberry-Timestamp nie jest starszy niż wybrane okno — 5 minut to bezpieczna wartość.
  3. Policz oczekiwany podpis po stronie serwera i porównaj w stałym czasie (constant-time compare) z tym z nagłówka.
  4. Podpisuj/weryfikuj na surowych bajtach body, a nie na ponownie zserializowanym JSON-ie. Każda różnica w białych znakach złamie HMAC.

Node.js (Express)

import express from "express";
import crypto from "node:crypto";

const SECRET = process.env.SCRIBERRY_SECRET;
const app = express();

// Kluczowe: potrzebujemy surowych bajtów body do weryfikacji podpisu.
app.use("/scriberry/webhook", express.raw({ type: "application/json" }));

function verify(req) {
  const sig = (req.headers["x-scriberry-signature"] || "").replace(
    /^sha256=/,
    "",
  );
  const ts = req.headers["x-scriberry-timestamp"];
  if (!sig || !ts) return false;

  const ageSec = Math.abs(Date.now() / 1000 - Number(ts));
  if (ageSec > 300) return false;

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${ts}.`)
    .update(req.body) // req.body to Buffer dzięki express.raw()
    .digest("hex");

  const a = Buffer.from(sig, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/scriberry/webhook", async (req, res) => {
  if (!verify(req)) return res.status(401).send("invalid signature");

  const payload = JSON.parse(req.body.toString("utf8"));

  // Idempotencja — jeden post nie powinien przejść dwa razy.
  if (await alreadyProcessed(payload.idempotency_key)) {
    return res.status(200).send("duplicate, skipped");
  }

  try {
    await publish(payload); // np. zapis do CMS-a, commit do repo, post na Slacka
    await markProcessed(payload.idempotency_key);
    res.status(200).send("ok");
  } catch (err) {
    console.error(err);
    res.status(500).send("publish failed");
  }
});

app.listen(3000);

Python (FastAPI)

import hmac, hashlib, time, os, json
from fastapi import FastAPI, Request, HTTPException

SECRET = os.environ["SCRIBERRY_SECRET"].encode()
app = FastAPI()

def verify(headers, raw_body: bytes) -> bool:
    sig = headers.get("x-scriberry-signature", "").removeprefix("sha256=")
    ts = headers.get("x-scriberry-timestamp", "")
    if not sig or not ts:
        return False

    if abs(time.time() - int(ts)) > 300:
        return False

    expected = hmac.new(
        SECRET,
        f"{ts}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(sig, expected)

@app.post("/scriberry/webhook")
async def webhook(request: Request):
    raw_body = await request.body()
    if not verify(request.headers, raw_body):
        raise HTTPException(status_code=401, detail="invalid signature")

    payload = json.loads(raw_body)

    if await already_processed(payload["idempotency_key"]):
        return {"status": "duplicate, skipped"}

    try:
        await publish(payload)  # np. zapis do CMS-a, commit do repo, post na Slacka
        await mark_processed(payload["idempotency_key"])
        return {"status": "ok"}
    except Exception:
        raise HTTPException(status_code=500, detail="publish failed")

Testowanie połączenia

Zanim podejdzie pierwszy realny termin publikacji, użyj wbudowanego testu:

  1. Na karcie integracji otwórz menu (trzy kropki) → Testuj połączenie.
  2. Scriberry wyśle na Twój endpoint testowy payload z dodatkowym polem _meta.is_test: true (timeout 10 sekund).
  3. W dialogu zobaczysz:
  4. kod HTTP odpowiedzi,
  5. czas round-tripu w ms,
  6. krótki fragment Twojego response body (do 500 znaków).

Custom request test endpoint

Test nie zmienia statusu integracji ani jej ostatniego błędu — nieudany test to wskazówka, a nie błąd krytyczny. Realne publikacje mają niezależny mechanizm ponawiania i raportowania błędów.

Co dzieje się przy publikacji

  1. Co kilka minut Scriberry sprawdza, które zatwierdzone posty mają termin publikacji w przeszłości i nie zostały jeszcze opublikowane przez tę integrację.
  2. Dla każdego z nich kolejkujemy zadanie publikacji.
  3. Worker odszyfrowuje Twój sekret, składa payload (w wybranym formacie), podpisuje go i wysyła POST na Twój endpoint z nagłówkami opisanymi wyżej.
  4. Odpowiedź 2xx → publikacja jest oznaczona jako wykonana; licznik i znacznik ostatniej synchronizacji na karcie integracji się aktualizują.
  5. Odpowiedź 4xx/5xx, błąd sieci albo timeout → ponawiamy żądanie z rosnącym odstępem. Po wyczerpaniu prób publikacja jest oznaczona jako nieudana, a treść błędu pojawia się na karcie integracji.

Wybór formatu treści — HTML czy Markdown

Format Kiedy wybrać
HTML Większość CMS-ów — WordPress (własny), Notion, Webflow CMS API, wszystko co konsumuje HTML wprost.
Markdown Generatory stron statycznych (Hugo, Astro, Eleventy), publikacja przez Git, narzędzia wewnętrzne preferujące źródłową postać treści.

W obu wariantach obrazy w treści są wskazane jako bezwzględne adresy URL na naszym CDN-ie. Możesz zostawić je w tej formie albo pobrać i przepisać linki na własny storage przed publikacją.

Rotacja sekretu

Użyj Wygeneruj nowe dane logowania z menu integracji. Poprzedni sekret przestaje działać natychmiast, więc:

  1. Wygeneruj nowy sekret.
  2. Zaktualizuj go po swojej stronie (zmienna środowiskowa, sekret w secret managerze itp.).
  3. Zrestartuj proces lub przeładuj konfigurację — w zależności od tego, jak masz to zorganizowane.

Status integracji nie zmienia się — pozostaje Aktywna.

Custom request refresh token result

Diagnostyka

  • Stale 401 invalid signature — najczęstsza przyczyna to weryfikacja podpisu na sparsowanym i ponownie zserializowanym body. Podpisuj surowe bajty. W Expressie użyj express.raw() zamiast express.json() dla tego endpointu. Druga częsta przyczyna: niesynchronizowany zegar serwera — sprawdź NTP.
  • Timeouty — Twój handler robi za dużo synchronicznie (np. pobiera obrazy z CDN-u przy każdym żądaniu). Odpowiedz 200 szybko, a ciężką robotę przerzuć do kolejki.
  • Duplikaty — implementuj idempotencję na podstawie idempotency_key. Bez tego ponawiana publikacja może się pojawić u Ciebie kilka razy.
  • Test działa, ale realne publikacje nie — sprawdź, czy na produkcyjnym endpointcie zaktualizowałeś sekret po jego rotacji.