Siirry sisältöön

Webhookit

Webhookit ilmoittavat sovelluksellesi tapahtumista reaaliaikaisesti sen sijaan, että joutuisit pollaamaan tunnuspalvelua. Webhook luodaan tenantin admin-paneelissa (ks. Webhookit hallinnassa), jossa myös näet toimitushistorian ja voit uusintayrittää epäonnistuneita toimituksia.

Tapahtuma Milloin laukeaa
login.success Käyttäjä kirjautuu onnistuneesti sisään (authorization coden myöntöhetkellä)
org.member.added Käyttäjä lisätään organisaatioon — suoraan tai kutsun hyväksynnän kautta
org.member.removed Käyttäjä poistetaan organisaatiosta
user.updated Käyttäjän sähköpostiosoite vaihtuu, joko käyttäjän omasta tilinäkymästä tai admin-paneelista. Hae tuoreet tiedot /userinfo-endpointista tai machine API:sta
webhook.ping Manuaalinen testipingi admin-paneelin “Ping”-painikkeesta

Jokainen toimitus on POST-pyyntö rekisteröityyn URL-osoitteeseen.

// login.success
{ "user_id": "01923f7a-...", "tenant_id": "5e3b1a2c-..." }
// org.member.added
{ "org_id": "5e3b1a2c-...", "user_id": "01923f7a-...", "role": "admin" }
// org.member.removed
{ "org_id": "5e3b1a2c-...", "user_id": "01923f7a-..." }
// user.updated
{ "user_id": "01923f7a-..." }
// webhook.ping
{
"event": "webhook.ping",
"tenant_id": "5e3b1a2c-...",
"endpoint_id": "8f2a...",
"triggered_by": "01923f7a-..."
}
Header Arvo
X-Tunnuspalvelu-Event Tapahtumatyyppi, esim. login.success
X-Tunnuspalvelu-Timestamp Unix-aika sekunteina
X-Tunnuspalvelu-Signature sha256=<hex>

Kanoninen allekirjoitusmerkkijono:

{TIMESTAMP}\n{EVENT_TYPE}\n{BODY}

BODY on tarkalleen lähetetty JSON-runko merkkijonona. Allekirjoitus on HMAC-SHA256, avaimena webhookin oman, admin-paneelissa kertaalleen näytetyn secretin.

// Node.js / TypeScript
import crypto from "crypto";
function verifyWebhook(
secret: string,
timestamp: string,
eventType: string,
body: string,
signature: string,
): boolean {
// Hylkää yli 5 minuuttia vanhat toimitukset
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp, 10));
if (age > 300) return false;
const msg = `${timestamp}\n${eventType}\n${body}`;
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(msg).digest("hex");
try {
return crypto.timingSafeEqual(
Buffer.from(expected, "utf8"),
Buffer.from(signature, "utf8"),
);
} catch {
return false; // pituusero — ei validi vertailu
}
}
# Python
import hashlib, hmac, time
def verify_webhook(secret: str, timestamp: str, event_type: str,
body: str, signature: str) -> bool:
age = abs(time.time() - int(timestamp))
if age > 300:
return False
msg = f"{timestamp}\n{event_type}\n{body}".encode()
expected = "sha256=" + hmac.new(
secret.encode(), msg, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)

Käytä aina ajassa vakioviivevertailua (timingSafeEqual / compare_digest), älä tavallista ===-vertailua.

app/api/webhooks/tunnuspalvelu/route.ts
import { NextRequest, NextResponse } from "next/server";
const WEBHOOK_SECRET = process.env.TUNNUSPALVELU_WEBHOOK_SECRET!;
export async function POST(req: NextRequest) {
const body = await req.text();
const timestamp = req.headers.get("x-tunnuspalvelu-timestamp") ?? "";
const eventType = req.headers.get("x-tunnuspalvelu-event") ?? "";
const signature = req.headers.get("x-tunnuspalvelu-signature") ?? "";
if (!verifyWebhook(WEBHOOK_SECRET, timestamp, eventType, body, signature)) {
return NextResponse.json({ error: "invalid signature" }, { status: 401 });
}
const payload = JSON.parse(body);
switch (eventType) {
case "login.success":
// payload.user_id, payload.tenant_id
break;
case "org.member.added":
// payload.org_id, payload.user_id, payload.role
break;
case "org.member.removed":
// payload.org_id, payload.user_id
break;
}
return NextResponse.json({ ok: true });
}
  • Toimitus on at-least-once: taustaprosessi pollaa odottavia toimituksia 5 sekunnin välein, korkeintaan 50 kerrallaan.
  • Epäonnistunut toimitus (muu kuin 2xx-vastaus tai timeout) uusintayritetään eksponentiaalisella viiveellä: 60 s → 5 min → 30 min → 6 h → 24 h, korkeintaan 5 yritystä. Viidennen epäonnistuneen yrityksen jälkeen toimitus ei enää uusiudu.
  • Jos endpoint vastaa toistuvasti 404:llä tai 410:llä, tunnuspalvelu poistaa webhookin käytöstä automaattisesti — tarkista tila admin-paneelissa, jos toimitukset lakkaavat yllättäen saapumasta.
  • Endpointin URL-osoite validoidaan uudelleen jokaisella toimituskerralla (ei vain rekisteröinnin yhteydessä) DNS-uudelleenohjaushyökkäysten torjumiseksi — sisäverkkoon tai localhostiin osoittava URL hylätään.

Klikkaa “Ping” webhookin admin-paneelisivulla (/admin/webhooks/{id}) — lähettää webhook.ping-tapahtuman todelliseen toimitusjonoon, jotta voit varmistaa allekirjoituksen tarkistuksen toimivan ilman että odotat oikeaa käyttäjätapahtumaa.