Dokumentacja API

Trzy endpointy, jeden nagłówek uwierzytelniający, rozliczenie za rozstrzygnięte cytowanie. Poniżej wszystko, czego potrzeba do integracji — każdy przykład został uruchomiony na produkcji.

Uwierzytelnianie

Klucz w nagłówku Bearer. Nieznany, nieaktywny i brakujący klucz dają ten sam wynik 401 — nie podpowiadamy, która z tych rzeczy zawiodła.

Authorization: Bearer cyt_live_…

Dwa rodzaje kluczy

Klucz z prefiksem cyt_test_ korzysta z tego samego korpusu i zwraca te same odpowiedzi, ale nie jest rozliczany — ${CYTOWANIA_TESTOWE} cytowań miesięcznie na integrację i sprawdzenie obsługi błędów. Klucz cyt_live_ obsługuje ruch produkcyjny i liczy się do pakietu. Klucz widzisz jeden jedyny raz przy wydaniu; w bazie trzymamy wyłącznie jego skrót.

Za co płacisz

Jednostką rozliczeniową jest ROZSTRZYGNIĘTE CYTOWANIE — jedno zdanie, do którego znaleźliśmy źródło z numerem strony. Odmowa (found: false) jest bezpłatna, a w /chapter-sources bezpłatny jest też materiał TŁA (trafność poniżej 7) — płacisz wyłącznie za źródła wprost o temacie. Osobno liczy się NOWY TEMAT: zbudowanie korpusu dla zagadnienia, którego jeszcze nie mamy — trwa minuty i kosztuje dwa rzędy wielkości więcej, dlatego uruchamiasz je świadomie przez harvestOnMiss. Powyżej pakietu wywołania działają dalej po stawkach nadwyżki (doliczane do następnej faktury) — ale najwyżej do DODATKOWYCH 100% pakietu; potem 429 do 1. dnia miesiąca albo do przejścia na wyższy pakiet. Rachunek nie może cię więc zaskoczyć bardziej niż dwukrotnością pakietu.

PakietCena / mies.CytowaniaNowe tematyPonad pakiet
Testza darmo1003
Basic$194005$0.04 / $1.00
Start$49120015$0.04 / $1.00
Pro$99300040$0.04 / $1.00

Endpointy

GET /usage

Stan pakietu i zużycie w bieżącym miesiącu. Nic nie kosztuje i niczego nie zużywa — najtańszy sposób sprawdzenia, czy klucz działa.

curl -s -H "Authorization: Bearer $CYTADO_KEY" \
  https://cytado.com/api/ext/usage
import os, requests

r = requests.get(
    "https://cytado.com/api/ext/usage",
    headers={"Authorization": f"Bearer {os.environ['CYTADO_KEY']}"},
    timeout=60,
)
print(r.json())
const r = await fetch("https://cytado.com/api/ext/usage", {
  headers: { Authorization: `Bearer ${process.env.CYTADO_KEY}` },
});
console.log(await r.json());

POST /find-source

Zdanie na wejściu, źródło z numerem strony na wyjściu. To jedyne wywołanie, za które płacisz — i tylko wtedy, gdy coś znajdzie. Do 60 pozycji w żądaniu; nadmiar odrzucamy JAWNIE. Pole hint to temat pracy: bez niego pytamy „czy to źródło potwierdza to zdanie”, a z nim „czy ktoś piszący TĘ pracę mógłby je zacytować”. Każde trafienie ma pole wsparcie — czy ZWRÓCONY fragment sam broni tezy: wprost (cytuj śmiało), posrednie (przeczytaj i zaznacz kontekst), brak (fragment nie stawia tego twierdzenia — poszukaj innego miejsca), null (nie oceniono).

curl -s -X POST https://cytado.com/api/ext/find-source \
  -H "Authorization: Bearer $CYTADO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "id": "a1",
      "claim": "Procrastination is accompanied by guilt and psychological discomfort.",
      "hint": "procrastination among university students"
    }
  ],
  "langs": [
    "en"
  ],
  "harvestOnMiss": false
}'
import os, requests

r = requests.post(
    "https://cytado.com/api/ext/find-source",
    headers={"Authorization": f"Bearer {os.environ['CYTADO_KEY']}"},
    json={
  "items": [
    {
      "id": "a1",
      "claim": "Procrastination is accompanied by guilt and psychological discomfort.",
      "hint": "procrastination among university students"
    }
  ],
  "langs": [
    "en"
  ],
  "harvestOnMiss": False
},
    timeout=600,
)
r.raise_for_status()
print(r.json())
const r = await fetch("https://cytado.com/api/ext/find-source", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CYTADO_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "items": [
    {
      "id": "a1",
      "claim": "Procrastination is accompanied by guilt and psychological discomfort.",
      "hint": "procrastination among university students"
    }
  ],
  "langs": [
    "en"
  ],
  "harvestOnMiss": false
}),
});
if (!r.ok) throw new Error(`cytado ${r.status}: ${await r.text()}`);
console.log(await r.json());

POST /chapter-sources

Temat rozdziału na wejściu, zestaw cytowalnych źródeł na wyjściu, uszeregowany pokryciem tematu. Pole exclude pozwala podać źródła już wykorzystane — pokrycie liczymy wtedy po tych, które zostały, więc kolejne rozdziały nie dostają w kółko tego samego. Naliczamy WYŁĄCZNIE źródła wprost o temacie (trafność ≥ 7) — i domyślnie tylko takie zwracamy; materiał tła (trafność 5-6, gratis) dostaniesz po jawnym minRelevance: 5. Każda pozycja ma pole wsparcie: czy zwracany fragment sam broni rozdziału (wprost / posrednie / brak, null = nie oceniono). Pole chapter jest opcjonalne: bez niego źródła dobierane są pod cały temat, z nim oceny kotwiczą się w tym rozdziale. limit ma sufit 15; limit: 0 (albo ujemny) to bezpłatny no-op — naturalny dla pętli `limit: docelowo - mam`.

curl -s -X POST https://cytado.com/api/ext/chapter-sources \
  -H "Authorization: Bearer $CYTADO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "topic": "screen time in preschool children",
  "chapter": "Introduction",
  "exclude": []
}'
import os, requests

r = requests.post(
    "https://cytado.com/api/ext/chapter-sources",
    headers={"Authorization": f"Bearer {os.environ['CYTADO_KEY']}"},
    json={
  "topic": "screen time in preschool children",
  "chapter": "Introduction",
  "exclude": []
},
    timeout=600,
)
r.raise_for_status()
print(r.json())
const r = await fetch("https://cytado.com/api/ext/chapter-sources", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CYTADO_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "topic": "screen time in preschool children",
  "chapter": "Introduction",
  "exclude": []
}),
});
if (!r.ok) throw new Error(`cytado ${r.status}: ${await r.text()}`);
console.log(await r.json());

Kształt odpowiedzi /find-source

{
  "results": [
    {
      "id": "a1",                      // twoje id, oddane bez zmian
      "claim": "…",                    // echo tezy, o która pytales
      "found": true,
      "source": {                      // UWAGA: pola zrodla sa TUTAJ, nie plasko
        "title": "…",
        "authors": "…",
        "year": 2021,
        "url": "https://doi.org/10.12740/pp/95085",
        "doi": "10.12740/pp/95085",
        "page_from": 1088,
        "page_to": 1090,
        "snippet": "…",
        "relevance": 9
      }
    },
    { "id": "a2", "claim": "…", "found": false }
  ],
  "billing": { "citations_charged": 1, "topics_charged": 0 },
  "harvested": 0
}

Brak źródła to po prostu brak pola source. Sześćdziesiąt pozycji w jednym żądaniu jest w porządku, ale licz około 1,6 s na tezę — komplet zajmie ~95 s. Ustaw timeout klienta z zapasem; brama daje 800 s.

Pola odpowiedzi, które warto rozumieć

found
false znaczy, że korpus nie ma czym poprzeć tej tezy. Nie zwracamy wtedy najbliższego dopasowania i nie pobieramy opłaty.
page_from / page_to
Numer strony DRUKOWANEJ — ten sam, na który trafi promotor sprawdzający przypis, a nie numer kartki w pliku PDF.
page_from_label
Wypełnione tylko przy numeracji nietypowej (np. „E136”). Puste pole przy wypełnionym page_from znaczy „numer drukowany to po prostu 152”, a nie „brak paginacji”.
relevance
9–10: wprost o tej tezie i tej grupie. 7–8: to samo zjawisko w pokrewnym ujęciu — inna grupa zawodowa, inny wiek, inny kraj; cytowalne, ale zaznacz kontekst.
url
Odnośnik do oryginału — DOI albo bezpośredni adres. Ma go KAŻDE zwrócone źródło; przy ponad połowie korpusu prowadzi do pełnego tekstu, bo są to publikacje otwarte.
snippet
Krótki fragment w ramach dozwolonego użytku. Pełnych tekstów źródeł, których nie wolno nam republikować, nie zwracamy nigdy — zwracamy odnośnik, pod którym czytelnik przeczyta je u wydawcy.
billing
Co dokładnie policzyliśmy w tym żądaniu: citations_charged (naliczone), already_paid (opłacone wcześniej w tym temacie) oraz free_background — ile zwróconych pozycji to bezpłatny materiał tła (trafność poniżej 7). Zero w free_background znaczy, że wszystko, co wróciło, było wprost o temacie.

Limity i błędy

401 unauthorized
Nieznany, nieaktywny albo brakujący klucz.
400 invalid-json
Ciało żądania nie jest poprawnym JSON-em.
400 missing-items
Brakuje wymaganego pola `items` (tablica).
400 unknown-field
Nieznane pole w żądaniu — z podpowiedzią, jeśli wygląda na literówkę (`min_relevance` → „czy chodziło o minRelevance?”). Nie ignorujemy po cichu pól, których nie znamy: literówka w nazwie parametru nie może cicho kosztować pieniędzy.
422 invalid_request
Żądanie poprawne składniowo, ale zła WARTOŚĆ znanego pola — odpowiedź wskazuje pole. Np. temat krótszy niż 8 albo dłuższy niż 4000 znaków, langs niebędące tablicą znanych kodów, exclude niebędące tablicą napisów, minRelevance niebędące liczbą.
429 rate_limited
Przekroczony limit. Nagłówek Retry-After mówi, za ile sekund wrócić. Bezpiecznik minutowy mówi „zwolnij”, limit dobowy „na dziś koniec”, sufit nadwyżki (pakiet + 100%) „do 1. dnia miesiąca albo wyższy pakiet” — komunikat je rozróżnia.
402
Wyczerpany pakiet na operacji, która wymaga opłaconego dostępu.

Długie operacje: harvest i odpytywanie

Odczyt gotowego korpusu trwa kilka sekund. Zbudowanie korpusu dla tematu, którego jeszcze nie mamy, to 5-30 minut: pobieramy i czytamy prawdziwe pliki PDF. Nie da się na to czekać w jednym żądaniu HTTP — po drodze stoi nginx z własnym limitem, a Twój klient ma swój. Dlatego /corpus ma tryb asynchroniczny: zlecasz i odpytujesz.

# 1. Enqueue. Returns immediately.
curl -s https://cytado.com/api/ext/corpus \
  -H "Authorization: Bearer $CYTADO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"topic":"persuasion in advertising","langs":["en"],"minCoverage":6,"async":true}'

# {"jobId":82,"coverage":7,"coverageWprost":1,"corpusReady":false,"async":true}
# jobId = null  ->  corpus already sufficient, nothing was started, nothing billed.

# 2. Poll every 20-30 s. Free: polling is never billed.
curl -s https://cytado.com/api/ext/corpus/status \
  -H "Authorization: Bearer $CYTADO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jobId":82,"topic":"persuasion in advertising","langs":["en"]}'

# {"jobId":82,"status":"running","ready":false,"coverage":9,"coverageWprost":4}
# status: queued | running | done | failed | unknown
# stop when "ready" is true, then call /chapter-sources

Odpytuj co 20-30 sekund i przewiduj limit własnego oczekiwania — status failed jest legalną odpowiedzią, nie awarią połączenia. Gdy zadanie padnie, korpus zostaje taki, jaki był, i możesz pracować na tym, co już masz.

To samo w Pythonie — gotowe do skopiowania:

import os, time, requests

BASE = "https://cytado.com/api/ext"
HEAD = {"Authorization": f"Bearer {os.environ['CYTADO_KEY']}"}

def ensure_corpus(topic, langs=("pl", "en"), min_coverage=6, limit_s=1800):
    """Ensure the corpus covers a topic. Returns final coverage.

    Blocks for up to limit_s. A harvest is billed once as a New topic;
    polling is free. jobId None means the corpus was already sufficient
    and nothing was started or charged.
    """
    r = requests.post(f"{BASE}/corpus", headers=HEAD, timeout=120, json={
        "topic": topic, "langs": list(langs),
        "minCoverage": min_coverage, "async": True,
    })
    r.raise_for_status()
    state = r.json()
    if not state.get("jobId"):
        return state.get("coverage", 0)          # already covered, no charge

    deadline = time.time() + limit_s
    while time.time() < deadline:
        time.sleep(25)                          # 20-30 s is the sane cadence
        try:
            s = requests.post(f"{BASE}/corpus/status", headers=HEAD, timeout=60,
                              json={"jobId": state["jobId"], "topic": topic,
                                    "langs": list(langs)})
            s.raise_for_status()
        except requests.RequestException:
            continue                            # transient: keep polling
        state2 = s.json()
        if state2.get("ready"):                 # done OR failed — both final
            return state2.get("coverage", 0)
    return state.get("coverage", 0)              # our deadline, not an error

def chapter_sources(topic, chapter, used=(), limit=12):
    """Sources for one chapter. Pass previously used URLs/DOIs in 'used'
    so the next chapter gets NEW material instead of the same top hits."""
    r = requests.post(f"{BASE}/chapter-sources", headers=HEAD, timeout=600, json={
        "topic": topic, "chapter": chapter, "langs": ["pl", "en"],
        "limit": limit, "exclude": list(used),
    })
    r.raise_for_status()
    return r.json()

ensure_corpus("persuasion in advertising")
data = chapter_sources("persuasion in advertising", "Chapter 1")
print(data["coverage"], data["coverageWprost"], data["billing"])

Windows i PowerShell: wyślij ciało jako UTF-8

PowerShell 5.1 przy Invoke-RestMethod -Body <napis> koduje ciało domyślną stroną kodową systemu, nie UTF-8. Znaki spoza ASCII — polskie ogonki, niemieckie umlauty, cyrylica — giną, zanim żądanie wyjdzie z komputera. Temat dociera do nas zniekształcony, wyszukiwanie działa na złym tekście, a nic tego nie sygnalizuje. Przekaż BAJTY zamiast napisu:

$body  = @{ topic = "calibration of pressure transducers" } | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($body)   # <-- required
Invoke-RestMethod -Uri "$BASE/corpus" -Method Post -Headers $h -Body $bytes

PowerShell 7 i curl.exe robią to poprawnie same. Problem dotyczy wyłącznie wbudowanego PowerShella 5.1 z Windowsa.

Kolejne rozdziały: parametr exclude

Bez exclude drugi rozdział dostanie te same najlepsze źródła, co pierwszy — bo obiektywnie są najlepsze. Przekaż URL-e albo DOI już zużytych pozycji, a pokrycie zostanie policzone po tym, czym da się jeszcze napisać KOLEJNY rozdział, nie po całym temacie.

curl -s https://cytado.com/api/ext/chapter-sources \
  -H "Authorization: Bearer $CYTADO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "topic": "persuasion in advertising",
        "chapter": "Chapter 2: emotional appeals",
        "langs": ["en"],
        "limit": 12,
        "exclude": ["https://doi.org/10.1234/abc", "10.5678/def"]
      }'

# {
#   "sources": [ ... ],
#   "coverage": 9,            # documents still available (excluded ones removed)
#   "coverageWprost": 5,      # of which are squarely ON the topic, not adjacent
#   "billing": { "citations_charged": 9, "already_paid": 3 }
# }

coverageWprost jest istotniejsze od coverage. Rozjazd „coverage 6, coverageWprost 0” znaczy, że korpus ma materiał z tej DZIEDZINY, ale nic o tej konkretnej rzeczy — i to jest moment, w którym warto zlecić harvest zamiast pisać z tego, co jest.

Sprawdź koszt, zanim zlecisz

To samo wywołanie /corpus może odczytać gotowy korpus (grosze) albo zbudować go od zera (nowy temat, kilkanaście minut). O tym, co się stanie, decyduje stan korpusu, którego z zewnątrz nie widzisz. Dodaj dryRun, a policzymy dokładnie tę samą bramkę i powiemy, co BY się stało — nie tworząc zadania i nie naliczając nic.

curl -s https://cytado.com/api/ext/corpus \
  -H "Authorization: Bearer $CYTADO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"topic":"persuasion in advertising","langs":["en"],
       "minCoverage":6,"dryRun":true}'

# {
#   "dryRun": true,
#   "coverage": 9,
#   "coverageWprost": 5,
#   "corpusReady": true,
#   "wouldHarvest": false,     # a real call would NOT start a harvest
#   "wouldBill": null          # ...and would bill nothing
# }
#
# When the corpus is thin:
#   "wouldHarvest": true,
#   "wouldBill": { "kind": "topic", "count": 1 }

Podgląd jest bezpłatny i nieograniczony. Używaj go w pętli decyzyjnej: najpierw dryRun, potem — jeśli koszt Ci odpowiada — to samo wywołanie bez tego pola.

Klucz testowy a nowe tematy

Klucz cyt_test_ może zbudować 3 NOWE TEMATY miesięcznie — tyle, żeby sprawdzić tryb asynchroniczny, odpytywanie i webhooki, czyli najtrudniejszą część integracji. Po wyczerpaniu tego przydziału odczyt korpusu i weryfikacja działa dalej bez zmian; zablokowane jest wyłącznie budowanie korpusu dla nowego tematu, bo to najdroższa operacja w całym API. Do ruchu produkcyjnego użyj klucza cyt_live_.

Bez Postmana: konsola w panelu

Wszystkie trzy endpointy — /corpus, /chapter-sources i /find-source — możesz wywołać z panelu (Konto → API → Konsola), bez wklejania klucza i bez konfigurowania czegokolwiek. Każde pole ma opis mówiący, co się stanie, a nie jakiego jest typu. Pod formularzem stoi to samo wywołanie jako curl, gotowe do skopiowania do kodu.

Koszt pokazujemy PRZED wysłaniem, ale w każdym endpoincie inaczej — bo w każdym bierze się skądinąd. /corpus ma dwustopniowy przycisk: pierwszy sprawdza koszt i nie może nic naliczyć, drugi (ze wskazaną ceną) odsłania się dopiero pod odpowiedzią. /chapter-sources i /find-source podają sufit od razu, bo ogranicza go liczba, którą sam wpisujesz: liczba źródeł albo liczba tez. Uwaga na jedno pole: harvestOnMiss w /find-source potrafi jednym kliknięciem zbudować do 24 nowych tematów, dlatego domyślnie jest wyłączone.

maxNewTopics — ogranicz własny rachunek

W /find-source pole harvestOnMiss uruchamia dociąganie źródeł z sieci dla tez, których nie da się rozstrzygnąć z gotowego korpusu. Każde takie dociągnięcie to NOWY TEMAT, czyli pozycja ~25× droższa od cytowania. Bez tego pola budujemy najwyżej 6 tematów na żądanie; twardy sufit serwera to 24. Podaj maxNewTopics, żeby ustawić własną granicę — także wyższą, aż do 24. Wartość spoza zakresu przycinamy do 0-24, zamiast odrzucać żądanie.

{ "items": [...], "langs": ["en"],
  "harvestOnMiss": true,
  "maxNewTopics": 3 }        # at most 3 new topics in this request

Czym konsola NIE jest

Konsola wchodzi w ten sam kod co żądanie z zewnątrz — te same limity, ten sam sufit klucza testowego, to samo naliczanie i ten sam wpis w logach. Wywołanie z panelu kosztuje dokładnie tyle samo co wywołanie z Twojego serwera; nie jest to tryb darmowy. Pomija za to warstwę HTTP, więc nie sprawdzi błędu w nagłówkach ani w serializacji ciała żądania — jeśli debugujesz problem transportowy, użyj curl-a spod formularza, bo tylko on przechodzi przez pełny stos.

Serwer MCP — dla agentów AI

Całe API jest też dostępne jako serwer MCP (Model Context Protocol) — agent podłącza się raz i dostaje narzędzia z opisami, które mówią mu, kiedy je wołać i co kosztuje. Ten sam klucz co REST, to samo naliczanie, to samo zużycie w panelu. Transport: Streamable HTTP, bez sesji.

# Claude Code
claude mcp add cytado --transport http https://cytado.com/api/mcp \
  --header "Authorization: Bearer $CYTADO_KEY"

# Claude API (MCP connector)
"mcp_servers": [{ "type": "url", "url": "https://cytado.com/api/mcp",
                  "name": "cytado", "authorization_token": "$CYTADO_KEY" }]

# stdio-only clients
npx -y mcp-remote https://cytado.com/api/mcp \
  --header "Authorization: Bearer $CYTADO_KEY"

Narzędzia MCP nigdy nie harvestują synchronicznie — ground_citation odpowiada z korpusu w sekundach, a dociąganie nowych tematów zawsze idzie w tle przez jobId. Semantyka rozliczeń stoi wprost w opisach narzędzi, więc agent wie, co kosztuje, zanim zawoła.

Podłączenie w oknie czatu (claude.ai)

cytado możesz dodać jako własny konektor (custom connector) bezpośrednio w claude.ai — wtedy narzędzia są dostępne w zwykłej rozmowie, bez terminala i bez kodu:

  1. W claude.ai wejdź w Ustawienia → Konektory (na planach Team/Enterprise robi to właściciel organizacji w ustawieniach organizacji) i kliknij „Dodaj własny konektor” (Add custom connector).
  2. Jako adres serwera podaj: https://cytado.com/api/mcp
  3. W sekcji nagłówków żądania (Request headers) dodaj nagłówek Authorization o wartości „Bearer ” + Twój klucz API z panelu (Konto → API). Claude zapisuje wartość bezpiecznie i wysyła ją przy każdym żądaniu.
  4. W rozmowie kliknij „+” → Konektory i włącz cytado. Od tej chwili możesz poprosić: „napisz akapit o X i ugruntuj twierdzenia w źródłach przez cytado” — agent sam zawoła narzędzia.

Uwaga: pole nagłówków żądania w claude.ai jest funkcją beta udostępnianą stopniowo — jeśli go nie widzisz, Twoje konto obsługuje na razie wyłącznie konektory OAuth. Logowanie OAuth dla cytado jest w przygotowaniu; do tego czasu użyj Claude Code albo konektora MCP w Claude API (przykłady wyżej). W ChatGPT konektor dodasz analogicznie w trybie deweloperskim (Settings → Connectors), podając ten sam adres i nagłówek.

Powiadomienia HTTP (webhooki)

Zamiast odpytywać nas o stan pakietu, możesz podać adres, na który wyślemy podpisany POST: przy 80% i 100% pakietu, przy zamknięciu okresu rozliczeniowego, po wystawieniu faktury oraz po zakończeniu dociągania korpusu. Adres i sekret ustawiasz w panelu.

POST https://your-app.example/cytado-hook
X-Cytado-Event: job.completed
X-Cytado-Timestamp: 1785858025
X-Cytado-Signature: 9f2c...          # HMAC-SHA256 over "<timestamp>.<raw body>"

{
  "event": "job.completed",
  "at": 1785858025,
  "data": {
    "jobId": 83,
    "topic": "persuasion in advertising",
    "coverage": 15,
    "coverageWprost": 10
  }
}

# Verify in Node — sign the RAW body, never re-serialised JSON:
import { createHmac, timingSafeEqual } from "node:crypto";

const expected = createHmac("sha256", SECRET)
  .update(`${req.headers["x-cytado-timestamp"]}.${rawBody}`)
  .digest("hex");
const provided = req.headers["x-cytado-signature"];
const ok =
  provided.length === expected.length &&
  timingSafeEqual(Buffer.from(expected), Buffer.from(provided));

Podpisuj RAW BODY, nie ponownie zserializowany JSON — przestawienie kluczy zmieni podpis. Odrzucaj żądania starsze niż kilka minut. Wysyłamy jedną próbę na zdarzenie: webhook jest wygodą, a nie źródłem prawdy — tym pozostaje panel i faktura.

Zmiany w API

Nie wersjonujemy adresów. Zamiast tego trzymamy się jednej obietnicy: pola nigdy nie znikają ani nie zmieniają znaczenia, a nowe mogą dochodzić w każdej chwili. Parsuj odpowiedzi tolerancyjnie — nieznane pole zignoruj, nie traktuj go jako błędu.

Co można bezpiecznie ponowić

/corpus/status
Bez ograniczeń — nie dotykają korpusu i nie są rozliczane.
/chapter-sources
Bezpiecznie. Dokumenty, za które już zapłaciłeś w tym temacie, nie naliczą się ponownie — zobaczysz je w polu billing.already_paid.
/corpus (async: true)
Bezpiecznie w oknie 30 minut: powtórzone zlecenie dla tego samego tematu zwróci TO SAMO jobId i nie naliczy drugiego tematu.
/find-source
Ponowienie po timeoucie może naliczyć rozstrzygnięte cytowania drugi raz — jeśli ponawiasz automatycznie, zapisuj u siebie, co już wróciło.

Zacznij od klucza testowego

100 cytowań miesięcznie, bez rozliczenia i bez karty. Klucz wydasz sobie sam w panelu, zaraz po założeniu konta.

Załóż konto