Įvykiai ir webhooks
Įvykių tipai, parašo tikrinimas, pakartojimai, paslapties rotacija ir įvykių srautas.
Du būdai sužinoti apie pokyčius
Kad nereikėtų nuolat tikrinti būsenos, Diko siūlo du būdus:
- Webhooks: Diko pati atsiunčia
POSTužklausą jūsų adresu, kai kas nors įvyksta. - Įvykių srautas:
GET /eventsleidžia jums pasiimti įvykius, kada patogu. Tai ir atsarginis kelias, jei jūsų webhook endpointas buvo nepasiekiamas.
Abu būdai naudoja tą patį objektą event.
Endpointo kūrimas
- Atidarykite
/settings/api/webhooksir pasirinkite Sukurti endpointą. Patvirtinkite veiksmą pakartotiniu prisijungimu, jei Diko to paprašys. - Įveskite HTTPS adresą ir pasirinkite įvykių tipus (arba visus).
- Nukopijuokite paslaptį
whsec_.... Ji rodoma tik vieną kartą. Laikykite ją paslapčių saugykloje. - Paspauskite Siųsti bandomąjį įvykį. Gausite
webhook.testtik šiam endpointui; jis nepatenka įGET /events.
Vienai paskyrai galima turėti daugiausia 10 endpointų. Endpointas gali būti enabled, disabled arba failing_disabled (automatiškai išjungtas dėl nuolatinių nesėkmių, žr. žemiau). Diko rodo paskutinį sėkmingą ir nesėkmingą pristatymą, 30 dienų pristatymų žurnalą su visų bandymų istorija ir leidžia siųsti pristatymą dar kartą.
Įvykių katalogas
Visų to paties resurso įvykių data turi bendrą bazę: object ir id. Likusius laukus nurodo lentelė.
| Tipas | Kada siunčiamas | Papildomi data laukai |
|---|---|---|
meeting.ready |
susitikimo apdorojimas baigtas | status, archived, minutes_status, minutes_version, run_id |
meeting.failed |
apdorojimas nepavyko arba nutrūko | status, archived, error_code, run_id |
meeting.minutes.updated |
protokolas atnaujintas (pergeneravimas, dalyvio pervadinimas, gramatikos patvirtinimas) | minutes_version |
meeting.updated |
pakeistas pavadinimas arba susitikimas užarchyvuotas | changed_fields (title, archived) |
meeting.deleted |
susitikimas ištrintas | – |
library.transcript.added |
transkripcija pridėta į biblioteką | youtube_video_id, source_type, added_at |
library.transcript.updated |
santrauka tapo ready arba failed |
changed_fields (["summary"]), summary_status |
library.transcript.removed |
transkripcija pašalinta iš bibliotekos | – |
library.import.succeeded |
importas baigtas | transcript_id, reused |
library.import.failed |
importas nepavyko | error_code |
library.import.canceled |
importas atšauktas | – |
webhook.test |
bandomasis įvykis, siunčiamas tik nurodytam endpointui ir nerodomas GET /events |
object (webhook_endpoint), id |
meeting.ready gali pasikartoti, jei susitikimas apdorojamas iš naujo: tada run_id kitoks. Gali atsirasti naujų įvykių tipų, todėl nežinomus tipus ignoruokite.
Pristatymo formatas
Diko siunčia POST su JSON turiniu, kuris yra event objektas. Turinys baitas į baitą tas pats kiekviename bandyme.
{
"id": "evt_3kQ9mXw0aB7nZp1RtYc4Ld",
"object": "event",
"type": "meeting.ready",
"timestamp": "2026-10-11T09:30:00.000Z",
"data": {
"object": "meeting",
"id": "mtg_0aZ1bY2cX3dW4eV5fU6gT7",
"status": "ready",
"archived": false,
"minutes_status": "ready",
"minutes_version": 2,
"run_id": "5b0e8f1a-2c3d-4e5f-8a9b-1c2d3e4f5a6b"
}
}Diko laikosi Standard Webhooks 1.0 specifikacijos. Kiekviena užklausa turi šias antraštes:
| Antraštė | Reikšmė |
|---|---|
webhook-id |
įvykio ID (evt_...), pastovus visiems bandymams |
webhook-timestamp |
Unix sekundės kiekvieno bandymo pradžioje |
webhook-signature |
v1,<base64 parašas>; rotacijos metu du parašai, atskirti tarpu |
Content-Type |
application/json |
User-Agent |
Diko-Webhooks/1.0 (+https://diko.app/en/api-docs/webhooks) |
Parašo tikrinimas
Visada tikrinkite parašą. Kitaip bet kas, žinantis jūsų adresą, galėtų siųsti suklastotus įvykius.
- Perskaitykite neapdorotą užklausos turinį (baitus, ne iš naujo sugeneruotą JSON).
- Patikrinkite
webhook-timestamp: skirtumas nuo dabartinio laiko turi būti ne didesnis kaip 5 minutės. Tai saugo nuo seno užklausos pakartojimo. - Pasirašymo raktas yra dekoduoti base64 baitai tos eilutės dalies, kuri eina po
whsec_. - Pasirašomas tekstas:
{webhook-id}.{webhook-timestamp}.{turinys}. - Parašas yra
base64(HMAC-SHA256(raktas, pasirašomas tekstas)). - Palyginkite su kiekvienu
v1,...parašu išwebhook-signaturepastoviu laiku (constant-time). Pakanka, kad vienas sutaptų.
import base64
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 5 * 60
def verify_webhook(secret: str, headers, raw_body: bytes) -> bool:
"""`secret` yra visa `whsec_...` eilutė, `headers` – nejautrus raidžių dydžiui žemėlapis."""
try:
message_id = headers["webhook-id"]
timestamp = headers["webhook-timestamp"]
signatures = headers["webhook-signature"]
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
except (KeyError, ValueError):
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{message_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
for candidate in signatures.split(" "):
version, _, signature = candidate.partition(",")
# Lyginkite baitus: compare_digest meta TypeError, jei eilutėje yra ne ASCII simbolių.
if version == "v1" and hmac.compare_digest(signature.encode(), expected.encode()):
return True
return Falseimport crypto from "node:crypto"
const TOLERANCE_SECONDS = 5 * 60
// `secret` yra visa "whsec_..." eilutė, `headers` – Node mažosiomis raidėmis rašomos antraštės,
// o `rawBody` – neapdorotas turinys kaip Buffer (kas kita atmetama).
export function verifyWebhook(secret, headers, rawBody) {
const id = headers["webhook-id"]
const timestamp = headers["webhook-timestamp"]
const signatures = headers["webhook-signature"]
if (!id || !timestamp || !signatures || !Buffer.isBuffer(rawBody)) return false
const sentAt = Number(timestamp)
if (!Number.isFinite(sentAt) || Math.abs(Date.now() / 1000 - sentAt) > TOLERANCE_SECONDS) return false
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64")
const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.`).update(rawBody).digest()
return signatures.split(" ").some(candidate => {
const [version, signature] = candidate.split(",")
if (version !== "v1" || !signature) return false
const received = Buffer.from(signature, "base64")
return received.length === expected.length && crypto.timingSafeEqual(received, expected)
})
}Naudojimas su FastAPI ir Express:
import json
import os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
@app.post("/diko")
async def diko_webhook(request: Request):
raw_body = await request.body()
if not verify_webhook(os.environ["DIKO_WEBHOOK_SECRET"], request.headers, raw_body):
raise HTTPException(status_code=400, detail="invalid signature")
event = json.loads(raw_body)
# Įdėkite į eilę, pašalinkite dublius pagal event["id"] ir atsakykite greitai.
return {"ok": True}import express from "express"
const app = express()
// express.raw palieka turinį nepakeistą; express.json() jį sugadintų parašui.
app.post("/diko", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyWebhook(process.env.DIKO_WEBHOOK_SECRET, req.headers, req.body)) return res.sendStatus(400)
const event = JSON.parse(req.body.toString("utf8"))
// Įdėkite į eilę, pašalinkite dublius pagal event.id ir atsakykite greitai.
res.sendStatus(200)
})Pristatymo garantijos
- Bent kartą (at-least-once). Tas pats įvykis gali atkeliauti kelis kartus. Šalinkite dublius pagal
webhook-id(tai tas pats kaipevent.id). - Eilės tvarka negarantuojama. Dėl pakartojimų senesnis įvykis gali atkeliauti vėliau. Be to, Diko siunčia iki dviejų to paties endpointo įvykių vienu metu, todėl net be pakartojimų du įvykiai gali atkeliauti atvirkščiai. Vertinkite
timestamparba perskaitykite resurso dabartinę būseną per API.GET /eventsyra vienintelė vieta, kur įvykiai išdėstyti tvarka (pagal užfiksavimą). - Atsakykite greitai. Pristatymas laikomas sėkmingu tik gavus 2xx. Atsakymo laukiama ne ilgiau kaip 15 sekundžių, todėl įvykį pirmiausia įdėkite į eilę, o sunkų darbą atlikite vėliau.
Pakartojimai
Kartojami: kiekvienas ne-2xx atsakymas, pasibaigęs skirtasis laikas (15 s) ir tinklo klaida. Iš viso daroma 8 bandymai per maždaug 41 valandą:
| Bandymas | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| Kada | iškart | po 1 min. | po 5 min. | po 30 min. | po 2 val. | po 5 val. | po 10 val. | po 24 val. |
Intervalai keičiami atsitiktine ±20 % paklaida. Kiekvienas bandymas gauna naują webhook-timestamp, todėl pakartojimas po 10 minučių vis tiek praeina 5 minučių tikrinimą. Turinys ir webhook-id nesikeičia.
- Diko atsižvelgia į antraštę
Retry-After(iki 1 valandos). - Diko neina pagal peradresavimus (3xx); jie laikomi nesėkme.
410 Gonereiškia „šio endpointo daugiau nėra“: Diko išjungia endpointą (disabled), atšaukia laukiančius pristatymus ir išsiunčia jums el. laišką.
Grandinės pertraukiklis ir automatinis išjungimas
Po 5 nesėkmių iš eilės Diko sustabdo siuntimą 1 minutei, o toliau pauzę dvigubina iki 2 valandų. Pasibaigus pauzei išsiunčiamas vienas bandomasis pristatymas. Jei endpointas nepavyksta ilgiau nei 72 valandas, jis tampa failing_disabled ir gaunate el. laišką (jame endpointas įvardijamas tik domenu ir ID; pilnas adresas lieka programėlėje).
Kai endpointą vėl įjungsite, nepavykusius pristatymus galite pakartoti programėlėje arba pasiimti praleistus įvykius iš GET /events.
Kai endpointą išjungiate jūs patys, jau laukiantys pristatymai lieka laukti ir išsiunčiami jį vėl įjungus. Kai po 72 valandų nesėkmių jį išjungia Diko, laukiantys pristatymai pažymimi kaip nepavykę, kad Pakartoti nepavykusius galėtų juos išsiųsti.
Sulaikyti pristatymai
Jei jūsų API prieiga sustabdyta arba planas nebeapima API, Diko pristatymus sulaiko: nieko nesiunčia ir nenaudoja bandymų. Patys įvykiai vis tiek įrašomi. Kol prieiga sustabdyta, pristatymai laukia neišsiųsti. Atšaukus sustabdymą, jie išsiunčiami maždaug per minutę. Jei planas nebeapima API, Diko iš naujo patikrina sulaikytus pristatymus po 15 minučių, o paskui vis rečiau. Laukimas ilgėja kartu su pristatymo amžiumi, iki 6 valandų. Kai planas vėl leidžia API, pristatymai išsiunčiami. Taip sulaikytas ilgiau nei 7 dienas pristatymas baigiasi kaip failed su plan_required. Atkūrę prieigą, galite jį išsiųsti iš pristatymų žurnalo mygtukais Siųsti dar kartą arba Pakartoti nepavykusius.
Paslapties rotacija
Rotuokite paslaptį puslapyje /settings/api/webhooks mygtuku Rotuoti paslaptį (reikia pakartotinio patvirtinimo). Nauja paslaptis rodoma vieną kartą, o sena galioja dar 24 valandas. Per tą laiką kiekvienas pristatymas turi du parašus antraštėje webhook-signature, atskirtus tarpu. Aukščiau pateiktas kodas tikrina visus, todėl:
- Sukurkite naują paslaptį.
- Įdiekite naują paslaptį ten, kur priimate webhooks.
- Palaukite, kol sena nustos galioti, arba patikrinkite, ar jūsų kodas jau naudoja naują.
Bandymas, pakartojimas ir žurnalas
Programėlėje galite: siųsti bandomąjį įvykį, peržiūrėti 30 dienų pristatymų žurnalą su kiekvieno bandymo būsena, pakartoti vieną pristatymą mygtuku Siųsti dar kartą ir Pakartoti nepavykusius už pasirinktą laikotarpį (iki 1 000 pristatymų). Bandomasis įvykis, pakartojimas ir masinis pakartojimas vienam endpointui ribojami iki 6 per minutę.
- Bandomasis įvykis (
webhook.test) gauna vieną bandymą be pakartojimų. Jis pristatomas net išjungtam endpointui, o nesėkmė neįskaitoma į grandinės pertraukiklį (sėkmė jį vis tiek atstato). - Pakartojant pristatymą, kurio 8 bandymai išnaudoti, jis išsiunčiamas dar lygiai vieną kartą; jei nepavyksta, lieka nepavykęs.
URL taisyklės ir SSRF apsauga
Diko siunčia užklausas iš savo serverių, todėl adresui taikomos griežtos taisyklės:
- tik
https, prievadas 443, be vartotojo vardo ir slaptažodžio URL; - negalima naudoti IP adreso: reikia domeno vardo su tašku;
- ne ilgesnis kaip 2048 simboliai;
- kiekvieno bandymo metu domenas išsprendžiamas iš naujo ir visi adresai turi būti vieši. Privatūs, vietiniai (loopback), link-local, multicast ir rezervuoti adresai atmetami, įskaitant į kitus formatus įterptus IPv4 (IPv4-mapped, 6to4, Teredo, NAT64);
- draudžiami hostai
localhost,*.local,metadata.google.internalir apskritai vidiniai serverių vardai (pavyzdžiui,*.internal); - jungtis prisegama prie patikrinto IP, TLS sertifikatas turi būti galiojantis, Diko neina pagal peradresavimus, o atsakymas skaitomas ne daugiau kaip 64 KB.
Įvykių srautas kaip alternatyva
GET /events leidžia pasiimti įvykius patiems. Tai tinka, jei negalite atverti viešo endpointo arba norite patikrinti, ar nepraleidote įvykių po prastovos. Pirmą kartą pradėkite nuo cursor=latest, o paskui saugokite paskutinį next_cursor. Diko užfiksuoja įvykius tik tada, kai paskyra turi neatšauktą raktą arba įjungtą webhook endpointą. Anksčiau pasibaigę susitikimai įvykių nesukuria, todėl pirmas paleidimas jų neatkuria:
import os
import time
from pathlib import Path
import requests
BASE = "https://api.diko.app/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DIKO_API_KEY']}"}
CURSOR_FILE = Path("diko-events.cursor") # laikykite ten, kur jūsų paslauga gali rašyti
def load_cursor():
return CURSOR_FILE.read_text().strip() if CURSOR_FILE.exists() else None
def save_cursor(cursor):
CURSOR_FILE.write_text(cursor)
def handle(event):
print(event["type"], event["id"]) # pakeiskite savo apdorojimu
cursor = load_cursor() or "latest" # išsaugotas žymeklis arba „nuo dabar“
while True:
response = requests.get(
f"{BASE}/events",
headers=HEADERS,
params={"cursor": cursor, "limit": 100, "types": ["meeting.ready"]},
timeout=30,
)
response.raise_for_status()
page = response.json()
for event in page["data"]:
handle(event) # idempotentiškai: dublius šalinkite pagal event["id"]
cursor = page["next_cursor"]
save_cursor(cursor)
if not page["has_more"]:
time.sleep(10)import { existsSync, readFileSync, writeFileSync } from "node:fs"
const BASE = "https://api.diko.app/api/v1"
const headers = { Authorization: `Bearer ${process.env.DIKO_API_KEY}` }
const CURSOR_FILE = "diko-events.cursor" // laikykite ten, kur jūsų paslauga gali rašyti
const loadCursor = () => (existsSync(CURSOR_FILE) ? readFileSync(CURSOR_FILE, "utf8").trim() : null)
const saveCursor = (cursor: string) => writeFileSync(CURSOR_FILE, cursor)
const handle = async (event: { id: string; type: string }) => console.log(event.type, event.id) // pakeiskite savo apdorojimu
let cursor: string = loadCursor() ?? "latest" // išsaugotas žymeklis arba „nuo dabar“
while (true) {
const query = new URLSearchParams({ cursor, limit: "100" })
query.append("types", "meeting.ready")
const response = await fetch(`${BASE}/events?${query}`, { headers })
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`)
const page: { data: { id: string; type: string }[]; next_cursor: string; has_more: boolean } = await response.json()
for (const event of page.data) await handle(event) // dublius šalinkite pagal event.id
cursor = page.next_cursor
await saveCursor(cursor)
if (!page.has_more) await new Promise(resolve => setTimeout(resolve, 10_000))
}Rodomi tik užfiksuoti įvykiai, išdėstyti užfiksavimo tvarka, todėl nė vienas nepraleidžiamas. Raktas mato tik tų šeimų įvykius, kuriuos gali skaityti pagal savo leidimus.