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)¶
- Twój programista przygotowuje na Waszej infrastrukturze adres internetowy (URL), pod który Scriberry będzie wysyłać posty.
- Ty w panelu Scriberry tworzysz integrację, podajesz ten adres i wybierasz format treści (HTML lub Markdown).
- Scriberry pokazuje Ci jednorazowo sekret podpisujący — przekazujesz go programiście, żeby Wasz serwer mógł sprawdzić, że żądanie naprawdę pochodzi od nas.
- 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¶
- Otwórz projekt → zakładka Integracje → kliknij kafelek Własny serwis (Custom HTTP).

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

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

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ć
POSTzContent-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:
Po stronie endpointu:
- Zdejmij prefiks
sha256=z nagłówkaX-Scriberry-Signature. - Sprawdź, czy
X-Scriberry-Timestampnie jest starszy niż wybrane okno — 5 minut to bezpieczna wartość. - Policz oczekiwany podpis po stronie serwera i porównaj w stałym czasie (constant-time compare) z tym z nagłówka.
- 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:
- Na karcie integracji otwórz menu (trzy kropki) → Testuj połączenie.
- Scriberry wyśle na Twój endpoint testowy payload z dodatkowym polem
_meta.is_test: true(timeout 10 sekund). - W dialogu zobaczysz:
- kod HTTP odpowiedzi,
- czas round-tripu w ms,
- krótki fragment Twojego response body (do 500 znaków).

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¶
- 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ę.
- Dla każdego z nich kolejkujemy zadanie publikacji.
- Worker odszyfrowuje Twój sekret, składa payload (w wybranym formacie), podpisuje go i wysyła
POSTna Twój endpoint z nagłówkami opisanymi wyżej. - Odpowiedź 2xx → publikacja jest oznaczona jako wykonana; licznik i znacznik ostatniej synchronizacji na karcie integracji się aktualizują.
- 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:
- Wygeneruj nowy sekret.
- Zaktualizuj go po swojej stronie (zmienna środowiskowa, sekret w secret managerze itp.).
- Zrestartuj proces lub przeładuj konfigurację — w zależności od tego, jak masz to zorganizowane.
Status integracji nie zmienia się — pozostaje Aktywna.

Diagnostyka¶
- Stale
401 invalid signature— najczęstsza przyczyna to weryfikacja podpisu na sparsowanym i ponownie zserializowanym body. Podpisuj surowe bajty. W Expressie użyjexpress.raw()zamiastexpress.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
200szybko, 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.