Skip to content
DikoAPI
API keys

Events and webhooks

Open as Markdown

Event types, signature verification, retries, secret rotation and the event stream.

Two ways to learn about changes

So you never have to poll for status, Diko offers two ways:

  • Webhooks: Diko sends a POST request to your address when something happens.
  • The event stream: GET /events lets you pick up events whenever it suits you. It is also the fallback when your webhooks were unreachable.

Both use the same event object.

Creating an endpoint

  1. Open /settings/api/webhooks and choose Sukurti endpointą (Create endpoint). Confirm the action by signing in again if Diko asks for it.
  2. Enter an HTTPS address and choose the event types (or all of them).
  3. Copy the secret whsec_.... It is shown only once. Keep it in a secrets store.
  4. Press Siųsti bandomąjį įvykį (Send test event). You get a webhook.test event for this endpoint only; it does not appear in GET /events.

You can have at most 10 endpoints per account. An endpoint can be enabled, disabled or failing_disabled (switched off automatically after persistent failures, see below). Diko shows the last successful and failed delivery, a 30-day delivery log with the history of every attempt, and lets you send a delivery again.

Event catalog

The data of all events about the same resource has a common base: object and id. The table lists the other fields.

Type When it is sent Extra data fields
meeting.ready meeting processing finished status, archived, minutes_status, minutes_version, run_id
meeting.failed processing failed or was interrupted status, archived, error_code, run_id
meeting.minutes.updated minutes updated (regeneration, speaker rename, grammar approval) minutes_version
meeting.updated the title changed or the meeting was archived changed_fields (title, archived)
meeting.deleted the meeting was deleted –
library.transcript.added a transcript was added to the library youtube_video_id, source_type, added_at
library.transcript.updated the summary became ready or failed changed_fields (["summary"]), summary_status
library.transcript.removed a transcript was removed from the library –
library.import.succeeded an import finished transcript_id, reused
library.import.failed an import failed error_code
library.import.canceled an import was canceled –
webhook.test a test event, sent only to the named endpoint and not shown in GET /events object (webhook_endpoint), id

meeting.ready can repeat when a meeting is processed again: then the run_id differs. New event types may appear, so ignore types you do not know.

Delivery format

Diko sends a POST with a JSON body that is the event object. The body is byte for byte the same on every attempt.

JSON
{
  "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 follows the Standard Webhooks 1.0 specification. Every request has these headers:

Header Value
webhook-id the event ID (evt_...), constant across attempts
webhook-timestamp Unix seconds at the start of each attempt
webhook-signature v1,<base64 signature>; during a rotation two signatures separated by a space
Content-Type application/json
User-Agent Diko-Webhooks/1.0 (+https://diko.app/en/api-docs/webhooks)

Verifying the signature

Always verify the signature. Otherwise anyone who knows your address could send forged events.

  1. Read the raw request body (the bytes, not re-serialised JSON).
  2. Check webhook-timestamp: the difference from the current time must be at most 5 minutes. This protects against replaying an old request.
  3. The secret key is the decoded base64 bytes of the part after whsec_.
  4. The signed content is {webhook-id}.{webhook-timestamp}.{body}.
  5. The signature is base64(HMAC-SHA256(key, signed content)).
  6. Compare it with every v1,... signature in webhook-signature using a constant-time comparison. One match is enough.
Python
import base64
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 5 * 60


def verify_webhook(secret: str, headers, raw_body: bytes) -> bool:
    """`secret` is the whole `whsec_...` string, `headers` a case-insensitive mapping."""
    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(",")
        # Compare bytes: compare_digest raises TypeError for a str with non-ASCII characters.
        if version == "v1" and hmac.compare_digest(signature.encode(), expected.encode()):
            return True
    return False

Using it with FastAPI and Express:

Python
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)
    # Queue it, remove duplicates by event["id"] and answer quickly.
    return {"ok": True}

Delivery guarantees

  • At least once. The same event can arrive several times. Remove duplicates by webhook-id (it equals event.id).
  • Order is not guaranteed. Because of retries an older event can arrive later, and Diko sends up to two events of one endpoint at the same time, so even without retries two events can arrive in the other order. Look at timestamp or read the current state of the resource through the API. GET /events is the one place where events are ordered (by commit).
  • Answer quickly. A delivery counts as successful only on a 2xx response. The timeout is 15 seconds, so queue the event first and do the heavy work later.

Retries

Every non-2xx response, timeout (15 s) and network error is retried. In total there are 8 attempts over about 41 hours:

Attempt 1 2 3 4 5 6 7 8
When at once after 1 min after 5 min after 30 min after 2 h after 5 h after 10 h after 24 h

Intervals vary by ±20 % random jitter. Every attempt gets a new webhook-timestamp, so a retry after 10 minutes still passes the 5-minute check. The body and webhook-id do not change.

  • The Retry-After header is honoured (up to 1 hour).
  • Redirects (3xx) are not followed and count as a failure.
  • 410 Gone means “this endpoint is gone”: Diko disables the endpoint (disabled), cancels pending deliveries and e-mails you.

Circuit breaker and automatic disabling

After 5 consecutive failures Diko pauses delivery for 1 minute and doubles the pause up to 2 hours. When a pause ends, one probe delivery is sent. If an endpoint keeps failing for more than 72 hours, it becomes failing_disabled and you get an e-mail (it names the endpoint by host and ID only; the full address stays in the app).

After you enable the endpoint again, you can replay the failed deliveries in the app or pick up the missed events from GET /events.

When you disable an endpoint, deliveries that are already waiting stay waiting and go out after you enable it again. When Diko disables it after 72 hours of failures, the waiting deliveries are marked failed so that Pakartoti nepavykusius (Replay failed) can send them.

Held deliveries

If your API access is suspended, or the plan no longer includes the API, Diko holds your deliveries: nothing is sent and no attempt is used up. The events themselves are still recorded. While access is suspended, deliveries wait unsent and go out within about a minute after the suspension is lifted. If the plan no longer includes the API, Diko looks at held deliveries again after 15 minutes, then less often (the wait grows with the delivery’s age, up to 6 hours), and sends them as soon as the plan allows it again. A delivery held that way for more than 7 days ends as failed with plan_required. Once access is back, you can send it from the delivery log with Siųsti dar kartą (Send again) or Pakartoti nepavykusius (Replay failed).

Secret rotation

Rotate the secret on /settings/api/webhooks with Rotuoti paslaptį (Rotate secret); this needs re-authentication. The new secret is shown once and the old one stays valid for another 24 hours. During that time every delivery carries two signatures in webhook-signature, separated by a space. The code above checks all of them, so:

  1. Create the new secret.
  2. Deploy the new secret to your receiver.
  3. Wait for the old one to expire, or check that your code already uses the new one.

Testing, replay and the log

In the app you can send a test event, view the 30-day delivery log with the status of every attempt, replay a single delivery with Siųsti dar kartą (Send again) and use Pakartoti nepavykusius (Replay failed) for a chosen period (up to 1,000 deliveries). Test events, replays and bulk replays are limited to 6 per minute per endpoint.

  • A test event (webhook.test) gets a single attempt and no retries. It is delivered even to a disabled endpoint, and a failure does not count towards the circuit breaker (a success still clears it).
  • Replaying a delivery whose 8 attempts are used up sends it exactly once more; if that fails it stays failed.

URL rules and SSRF protection

Diko sends requests from its own servers, so the address is subject to strict rules:

  • https only, port 443, no user name or password in the URL;
  • no IP addresses: a domain name with a dot is required;
  • at most 2048 characters;
  • on every attempt the domain is resolved again and every address must be public. Private, loopback, link-local, multicast and reserved addresses are rejected, including IPv4 embedded in other formats (IPv4-mapped, 6to4, Teredo, NAT64);
  • the hosts localhost, *.local, metadata.google.internal and internal host names in general (for example *.internal) are forbidden;
  • the connection is pinned to the verified IP, the TLS certificate must be valid, redirects are not followed and the response is read up to 64 KB at most.

The event stream as a fallback

GET /events lets you pick up events yourself. It suits you if you cannot expose a public endpoint or want to check that you missed nothing after downtime. Start with cursor=latest the first time, then store the last next_cursor. Diko records events only while the account has a key that is not revoked or an enabled webhook endpoint. Meetings that finished before that produce no events, so a first run does not back-fill them:

Python
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")  # keep it where your service can write


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"])  # replace with your own processing


cursor = load_cursor() or "latest"  # a stored cursor, or "from now"
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)  # idempotently: remove duplicates by event["id"]
    cursor = page["next_cursor"]
    save_cursor(cursor)
    if not page["has_more"]:
        time.sleep(10)

Only committed events are shown, in commit order, so none is skipped. A key sees only the events of the families it can read under its scopes.

Search for a topic, an endpoint or an error code.

Search across all documentation pagesesc close