Hendelser
Webhooks
Lag et endepunkt
Si hvor vi skal sende, og hvilke hendelser du vil ha. Alle hendelsene står i hendelseskatalogen.
curl -X POST "https://api.telkit.no/v1/webhook-endpoints" \ -H "Authorization: Bearer $TELKIT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: crm-endepunkt-1" \ -d '{ "url": "https://crm.example.no/telkit/webhooks", "events": [ "call.ended", "call.recording.ready", "call.transcript.ready", "sms.status" ], "description": "CRM-et"}'const response = await fetch("https://api.telkit.no/v1/webhook-endpoints", { method: "POST", headers: { Authorization: `Bearer ${process.env.TELKIT_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "crm-endepunkt-1", }, body: JSON.stringify({ url: "https://crm.example.no/telkit/webhooks", events: [ "call.ended", "call.recording.ready", "call.transcript.ready", "sms.status", ], description: "CRM-et", }),});const answer = await response.json();- Svaret har
secret, som starter medwhsec_. Den vises bare nå. Lagre den hos deg. - En profilnøkkel lager endepunkter for sin profil. En bedriftsnøkkel lager endepunkter for hele bedriften, eller for én profil med
profileId. modeerliveellertest. Et testendepunkt får bare hendelser fra testnøkler.- Du kan også lage og følge endepunktene i dashbordet under Utviklere.
Send en signert testhendelse for å se at alt virker:
curl -X POST "https://api.telkit.no/v1/webhook-endpoints/ep_0199e4ab9c7f7d6a88b5dfa07cbdce65/test" \ -H "Authorization: Bearer $TELKIT_API_KEY"const response = await fetch("https://api.telkit.no/v1/webhook-endpoints/ep_0199e4ab9c7f7d6a88b5dfa07cbdce65/test", { method: "POST", headers: { Authorization: `Bearer ${process.env.TELKIT_API_KEY}`, },});const answer = await response.json();Det vi sender
Bodyen er JSON med hendelsens id, navn, versjon, tidspunkt og innholdet i data.
{ "object": "event", "id": "evt_0199e4aa8b6e7c5f97a4ce9f6bacbd54", "type": "sms.status", "api_version": "1", "occurred_at": "2026-10-14T09:12:44.512Z", "data": { "eventId": "evt_0199e4aa8b6e7c5f97a4ce9f6bacbd54", "mode": "live", "profileId": "0199e4a2-7c1b-7d3e-9a41-5f2c8b6d1e07", "messageId": "0199e4a6-4d2a-7e1b-9c3f-8a5b2d6e7f10", "to": "+4791234567", "state": "delivered", "units": 1, "priceOre": 75, "errorDesc": null }}| Header | Betyr |
|---|---|
webhook-id | Leveringens id. Den samme ved hvert nytt forsøk av denne leveringen. |
webhook-timestamp | Når dette forsøket ble sendt, i sekunder siden 1970 (Unix-tid). |
webhook-signature | Én eller flere signaturer, skilt med mellomrom. |
Sjekk signaturen
Signaturen følger Standard Webhooks (standardwebhooks.com). Vi signerer teksten webhook-id.webhook-timestamp.body med HMAC-SHA256. Nøkkelen er hemmeligheten uten whsec_, base64-dekodet. Headeren har formen v1,<base64>.
- Les bodyen som rå tekst. Ikke parse og skriv den ut igjen før du sjekker.
- Avvis forsøket hvis
webhook-timestamper mer enn 5 minutter fra klokken din. - Regn ut signaturen og sammenlign med hver
v1-signatur i headeren.
import { createHmac, timingSafeEqual } from "node:crypto"; const toleranceSeconds = 5 * 60; /** True when the body came from telkit. `body` is the raw text, exactly as received. */export function verifyTelkitWebhook({ secret, headers, body }) { const id = headers.get("webhook-id"); const timestamp = headers.get("webhook-timestamp"); const signatures = headers.get("webhook-signature"); if (!id || !timestamp || !signatures) return false; const sentSeconds = Number(timestamp); const nowSeconds = Math.floor(new Date().getTime() / 1000); if (!Number.isInteger(sentSeconds)) return false; if (Math.abs(nowSeconds - sentSeconds) > toleranceSeconds) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key) .update(`${id}.${timestamp}.${body}`) .digest(); // The header may hold several signatures, separated by spaces: "v1,<base64> v1,<base64>". return signatures.split(" ").some((entry) => { const [version, signature] = entry.split(","); if (version !== "v1" || !signature) return false; const given = Buffer.from(signature, "base64"); return given.length === expected.length && timingSafeEqual(given, expected); });}Bibliotekene fra Standard Webhooks sjekker de samme signaturene, hvis du heller vil bruke et av dem.
Bytt hemmelighet
Når du bytter hemmelighet, signerer vi en stund med både den nye og den gamle. Da får du tid til å bytte hos deg uten å miste leveringer.
curl -X POST "https://api.telkit.no/v1/webhook-endpoints/ep_0199e4ab9c7f7d6a88b5dfa07cbdce65/rotate-secret" \ -H "Authorization: Bearer $TELKIT_API_KEY"const response = await fetch("https://api.telkit.no/v1/webhook-endpoints/ep_0199e4ab9c7f7d6a88b5dfa07cbdce65/rotate-secret", { method: "POST", headers: { Authorization: `Bearer ${process.env.TELKIT_API_KEY}`, },});const answer = await response.json();Ed25519
Vil du sjekke med en offentlig nøkkel i stedet for en delt hemmelighet, slår du på Ed25519 med PUT /v1/webhook-endpoints/:id/ed25519. Da får du en offentlig nøkkel (whpk_…), og hver levering får en v1a-signatur ved siden av v1.
Svar raskt, gjør jobben etterpå
Svar med en 2xx-status så fort signaturen er sjekket, og gjør resten i bakgrunnen. Alt annet enn 2xx, eller ingen svar, regner vi som et mislykket forsøk.
const seen = new Set(); // Use your database in production. export async function handleTelkitWebhook(request, secret) { const body = await request.text(); if (!verifyTelkitWebhook({ secret, headers: request.headers, body })) { return new Response("invalid signature", { status: 400 }); } const event = JSON.parse(body); if (!seen.has(event.id)) { seen.add(event.id); await queueForProcessing(event); // Do the work after you have answered. } return new Response(null, { status: 204 });}Hendelser om det samme objektet, for eksempel den samme samtalen, kommer i rekkefølge. Bruk likevel tidspunktet og tilstanden i hendelsen, og tål at en hendelse kommer to ganger.
Nye forsøk
Mislykkes en levering, prøver vi igjen etter 5 sekunder, 5 minutter, 30 minutter, 2 timer, 5 timer, 10 timer, 10 timer: 8 forsøk til sammen. Hvert forsøk har samme webhook-id og samme body, men nytt tidspunkt og ny signatur.
- Etter siste forsøk gir leveringen opp. Du finner den blant de mislykkede leveringene.
- Har et endepunkt feilet i fem døgn, eller svart
410 Gone, blir det slått av. Slå det på igjen medPOST /v1/webhook-endpoints/:id/revive.
curl "https://api.telkit.no/v1/webhook-endpoints/ep_0199e4ab9c7f7d6a88b5dfa07cbdce65/attempts" \ -H "Authorization: Bearer $TELKIT_API_KEY"const response = await fetch("https://api.telkit.no/v1/webhook-endpoints/ep_0199e4ab9c7f7d6a88b5dfa07cbdce65/attempts", { headers: { Authorization: `Bearer ${process.env.TELKIT_API_KEY}`, },});const answer = await response.json();Send på nytt
Når endepunktet virker igjen, sender du leveringene som ga opp:
curl "https://api.telkit.no/v1/webhook-endpoints/ep_0199e4ab9c7f7d6a88b5dfa07cbdce65/failed-deliveries" \ -H "Authorization: Bearer $TELKIT_API_KEY"const response = await fetch("https://api.telkit.no/v1/webhook-endpoints/ep_0199e4ab9c7f7d6a88b5dfa07cbdce65/failed-deliveries", { headers: { Authorization: `Bearer ${process.env.TELKIT_API_KEY}`, },});const answer = await response.json();curl -X POST "https://api.telkit.no/v1/webhook-endpoints/ep_0199e4ab9c7f7d6a88b5dfa07cbdce65/recover-failed" \ -H "Authorization: Bearer $TELKIT_API_KEY"const response = await fetch("https://api.telkit.no/v1/webhook-endpoints/ep_0199e4ab9c7f7d6a88b5dfa07cbdce65/recover-failed", { method: "POST", headers: { Authorization: `Bearer ${process.env.TELKIT_API_KEY}`, },});const answer = await response.json();Eller én bestemt hendelse til ett endepunkt:
curl -X POST "https://api.telkit.no/v1/events/evt_0199e4aa8b6e7c5f97a4ce9f6bacbd54/redeliver" \ -H "Authorization: Bearer $TELKIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "endpointId": "ep_0199e4ab9c7f7d6a88b5dfa07cbdce65"}'const response = await fetch("https://api.telkit.no/v1/events/evt_0199e4aa8b6e7c5f97a4ce9f6bacbd54/redeliver", { method: "POST", headers: { Authorization: `Bearer ${process.env.TELKIT_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ endpointId: "ep_0199e4ab9c7f7d6a88b5dfa07cbdce65", }),});const answer = await response.json();En levering som fortsatt prøver, eller som kom fram, kan ikke sendes på nytt på denne måten. Da svarer vi webhook_delivery_not_found. Gikk du glipp av noe likevel, les hendelsesloggen.