telybot Iets minder gewoon.
Veel slimmer.
NetherlandsEnglish Inloggen Praat met ons Gratis starten
Telybot-documentatieReferentie

Servervoorbeelden

Volledige, werkende code voor de twee manieren waarop Telybot met je server praat, in Node.js, Python, PHP, Go, Java, C# en Ruby: een actie die de bot tijdens een chat antwoord geeft, en een webhook die te horen krijgt wat er in de chat is gebeurd.

Kies je taal één keer: de voorbeelden hier en in de handleiding Acties volgen die keuze.

Wat het voorbeeld doet

  • POST /telybot/order-status beantwoordt een actie. De bot stuurt het bestelnummer en het e-mailadres die de klant heeft getypt. De server antwoordt 200 met de status van de bestelling als beide overeenkomen, en 404 als dat niet zo is, zodat de bot de klant vraagt te controleren wat hij heeft getypt.
  • POST /telybot/events ontvangt webhooks. Een nieuwe poging bevat dezelfde id, zodat elke gebeurtenis één keer wordt verwerkt, hoe vaak die ook binnenkomt, en het antwoord is meteen 204.
  • Beide weigeren met 401 een verzoek waarvan de handtekening niet klopt: een verzoek dat niet met je geheim is ondertekend, onderweg is gewijzigd of ouder is dan vijf minuten.

Voordat je begint

  1. Open Jouw bot → Acties → Beveiliging, druk onder Handtekening van Telybot op Sleutel tonen en kopieer het geheim. Het begint met whsec_.
  2. Geef het aan je server als de instelling TELYBOT_SIGNING_SECRET, uit een omgevingsvariabele of je opslag voor secrets. Zet het nooit in een webpagina of een app.
  3. Kies onder Acties → Nieuwe actie POST en het adres https://your-server.example/telybot/order-status, en vraag de klant om order_id (een getal) en email (een e-mailadres).
  4. Voeg onder Acties → Webhooks https://your-server.example/telybot/events toe met de gebeurtenissen die je wilt.

De code

// npm install express
const crypto = require('crypto');
const express = require('express');

const SECRET = process.env.TELYBOT_SIGNING_SECRET; // whsec_…, from Actions → Security

// True when your bot signed this request in the last five minutes.
function isFromTelyBot(headers, rawBody, secret) {
  const id = headers['webhook-id'];
  const timestamp = Number(headers['webhook-timestamp']);
  const now = Math.floor(Date.now() / 1000);
  if (!id || !(Math.abs(now - timestamp) <= 300)) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto.createHmac('sha256', key)
    .update(`${id}.${headers['webhook-timestamp']}.`).update(rawBody).digest();
  return String(headers['webhook-signature'] || '').split(' ').some((part) => {
    const given = Buffer.from(part.replace(/^v1,/, ''), 'base64');
    return part.startsWith('v1,') && given.length === expected.length
      && crypto.timingSafeEqual(given, expected);
  });
}

const app = express();
// The exact bytes Telybot signed: never parse and re-serialise before checking.
app.use('/telybot', express.raw({ type: () => true }));
const bodyOf = (req) => (Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0));

const orders = new Map([['48213', { email: 'asha@example.com', status: 'shipped', eta: '2 October' }]]);
const seen = new Set(); // ids of events already handled: keep these in your database

// An action: during a chat, the bot asks where an order is.
app.post('/telybot/order-status', (req, res) => {
  if (!isFromTelyBot(req.headers, bodyOf(req), SECRET)) return res.sendStatus(401);
  const asked = JSON.parse(bodyOf(req).toString('utf8'));
  const order = orders.get(asked.order_id);
  if (!order || order.email !== String(asked.email || '').trim().toLowerCase()) {
    return res.status(404).json({ error: 'not_found' }); // the bot asks the customer to check
  }
  res.json({ status: order.status, eta: order.eta });
});

// A webhook: something happened in a chat.
app.post('/telybot/events', (req, res) => {
  if (!isFromTelyBot(req.headers, bodyOf(req), SECRET)) return res.sendStatus(401);
  const event = JSON.parse(bodyOf(req).toString('utf8'));
  if (!seen.has(event.id)) { // a retry carries the same id
    seen.add(event.id);
    console.log(event.type, event.data); // hand it to your CRM or a queue
  }
  res.sendStatus(204);
});

app.listen(3000);
# pip install flask
import base64
import hashlib
import hmac
import json
import os
import time

from flask import Flask, request

SECRET = os.environ["TELYBOT_SIGNING_SECRET"]  # whsec_…, from Actions → Security


def is_from_telybot(headers, raw_body: bytes, secret: str) -> bool:
    """True when your bot signed this request in the last five minutes."""
    msg_id = headers.get("webhook-id", "")
    timestamp = headers.get("webhook-timestamp", "")
    try:
        if not msg_id or abs(time.time() - int(timestamp)) > 300:
            return False
    except ValueError:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())
    return any(
        part.startswith("v1,") and hmac.compare_digest(part[3:].encode(), expected)
        for part in headers.get("webhook-signature", "").split()
    )


app = Flask(__name__)
ORDERS = {"48213": {"email": "asha@example.com", "status": "shipped", "eta": "2 October"}}
seen = set()  # ids of events already handled: keep these in your database


# An action: during a chat, the bot asks where an order is.
@app.post("/telybot/order-status")
def order_status():
    raw = request.get_data()  # the exact bytes Telybot signed
    if not is_from_telybot(request.headers, raw, SECRET):
        return "", 401
    asked = json.loads(raw)
    order = ORDERS.get(asked.get("order_id", ""))
    if not order or order["email"] != asked.get("email", "").strip().lower():
        return {"error": "not_found"}, 404  # the bot asks the customer to check
    return {"status": order["status"], "eta": order["eta"]}


# A webhook: something happened in a chat.
@app.post("/telybot/events")
def events():
    raw = request.get_data()
    if not is_from_telybot(request.headers, raw, SECRET):
        return "", 401
    event = json.loads(raw)
    if event["id"] not in seen:  # a retry carries the same id
        seen.add(event["id"])
        print(event["type"], event["data"])  # hand it to your CRM or a queue
    return "", 204


if __name__ == "__main__":
    app.run(port=3000)
<?php
// Plain PHP 8. In Laravel, $request->getContent() is the raw body and
// $request->header('webhook-id') a header.

const ORDERS = ['48213' => ['email' => 'asha@example.com', 'status' => 'shipped', 'eta' => '2 October']];

/** True when your bot signed this request in the last five minutes. */
function is_from_telybot(string $id, string $timestamp, string $signatures, string $rawBody, string $secret): bool
{
    if ($id === '' || !preg_match('/^\d{1,12}$/', $timestamp) || abs(time() - (int) $timestamp) > 300) {
        return false;
    }
    $key = base64_decode(preg_replace('/^whsec_/', '', $secret));
    $expected = base64_encode(hash_hmac('sha256', "$id.$timestamp.$rawBody", $key, true));
    foreach (explode(' ', $signatures) as $part) {
        if (str_starts_with($part, 'v1,') && hash_equals($expected, substr($part, 3))) {
            return true;
        }
    }
    return false;
}

/** False when this event id was handled before: keep these in your database. */
function first_time(string $id): bool
{
    $marker = sys_get_temp_dir() . '/telybot-event-' . hash('sha256', $id);
    return ($handle = @fopen($marker, 'x')) !== false && fclose($handle);
}

$raw = file_get_contents('php://input'); // the exact bytes Telybot signed
$signed = is_from_telybot(
    $_SERVER['HTTP_WEBHOOK_ID'] ?? '',
    $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '',
    $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '',
    $raw,
    getenv('TELYBOT_SIGNING_SECRET'), // whsec_…, from Actions → Security
);
if (!$signed) {
    http_response_code(401);
    exit;
}

switch (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) {
    // An action: during a chat, the bot asks where an order is.
    case '/telybot/order-status':
        $asked = json_decode($raw, true);
        $order = ORDERS[$asked['order_id'] ?? ''] ?? null;
        header('Content-Type: application/json');
        if (!$order || $order['email'] !== strtolower(trim($asked['email'] ?? ''))) {
            http_response_code(404); // the bot asks the customer to check
            echo json_encode(['error' => 'not_found']);
            break;
        }
        echo json_encode(['status' => $order['status'], 'eta' => $order['eta']]);
        break;

    // A webhook: something happened in a chat.
    case '/telybot/events':
        $event = json_decode($raw, true);
        if (first_time($event['id'])) { // a retry carries the same id
            error_log($event['type'] . ' ' . json_encode($event['data'])); // hand it to your CRM or a queue
        }
        http_response_code(204);
        break;

    default:
        http_response_code(404);
}
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"
)

// isFromTelyBot reports whether your bot signed this request in the last five minutes.
func isFromTelyBot(h http.Header, body []byte, secret string) bool {
	id, timestamp := h.Get("webhook-id"), h.Get("webhook-timestamp")
	sent, err := strconv.ParseInt(timestamp, 10, 64)
	now := time.Now().Unix()
	if id == "" || err != nil || sent < now-300 || sent > now+300 {
		return false
	}
	key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
	if err != nil {
		return false
	}
	mac := hmac.New(sha256.New, key)
	mac.Write([]byte(id + "." + timestamp + "."))
	mac.Write(body)
	expected := mac.Sum(nil)
	for _, part := range strings.Fields(h.Get("webhook-signature")) {
		given, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(part, "v1,"))
		if strings.HasPrefix(part, "v1,") && err == nil && hmac.Equal(given, expected) {
			return true
		}
	}
	return false
}

type order struct{ Email, Status, ETA string }

var orders = map[string]order{"48213": {"asha@example.com", "shipped", "2 October"}}

var (
	mu   sync.Mutex
	seen = map[string]bool{} // ids of events already handled: keep these in your database
)

func main() {
	secret := os.Getenv("TELYBOT_SIGNING_SECRET") // whsec_…, from Actions → Security

	// signed reads the exact bytes Telybot signed and refuses anything else.
	signed := func(next func(http.ResponseWriter, []byte)) http.HandlerFunc {
		return func(w http.ResponseWriter, r *http.Request) {
			body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
			if err != nil || !isFromTelyBot(r.Header, body, secret) {
				w.WriteHeader(http.StatusUnauthorized)
				return
			}
			next(w, body)
		}
	}

	// An action: during a chat, the bot asks where an order is.
	http.HandleFunc("POST /telybot/order-status", signed(func(w http.ResponseWriter, body []byte) {
		var asked struct {
			OrderID string `json:"order_id"`
			Email   string `json:"email"`
		}
		json.Unmarshal(body, &asked)
		w.Header().Set("Content-Type", "application/json")
		o, ok := orders[asked.OrderID]
		if !ok || o.Email != strings.ToLower(strings.TrimSpace(asked.Email)) {
			w.WriteHeader(http.StatusNotFound) // the bot asks the customer to check
			json.NewEncoder(w).Encode(map[string]string{"error": "not_found"})
			return
		}
		json.NewEncoder(w).Encode(map[string]string{"status": o.Status, "eta": o.ETA})
	}))

	// A webhook: something happened in a chat.
	http.HandleFunc("POST /telybot/events", signed(func(w http.ResponseWriter, body []byte) {
		var event struct {
			ID   string          `json:"id"`
			Type string          `json:"type"`
			Data json.RawMessage `json:"data"`
		}
		json.Unmarshal(body, &event)
		mu.Lock()
		first := !seen[event.ID] // a retry carries the same id
		seen[event.ID] = true
		mu.Unlock()
		if first {
			log.Printf("%s %s", event.Type, event.Data) // hand it to your CRM or a queue
		}
		w.WriteHeader(http.StatusNoContent)
	}))

	log.Fatal(http.ListenAndServe(":3000", nil))
}
// Spring Boot 4 (spring-boot-starter-webmvc)
package com.example.telybot;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.json.JsonMapper;

@SpringBootApplication
@RestController
public class TelyBotReceiver {

    static final String SECRET = System.getenv("TELYBOT_SIGNING_SECRET"); // whsec_…, from Actions → Security
    static final Map<String, Map<String, String>> ORDERS =
            Map.of("48213", Map.of("email", "asha@example.com", "status", "shipped", "eta", "2 October"));
    final Set<String> seen = ConcurrentHashMap.newKeySet(); // ids of events already handled: keep these in your database
    final JsonMapper json = JsonMapper.builder().build();

    public static void main(String[] args) {
        SpringApplication.run(TelyBotReceiver.class, args);
    }

    /** True when your bot signed this request in the last five minutes. */
    static boolean isFromTelyBot(String id, String timestamp, String signatures, byte[] body, String secret)
            throws Exception {
        long now = Instant.now().getEpochSecond();
        long sent;
        try {
            sent = Long.parseLong(timestamp);
        } catch (NumberFormatException notANumber) {
            return false;
        }
        if (id == null || signatures == null || sent < now - 300 || sent > now + 300) return false;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(Base64.getDecoder().decode(secret.replaceFirst("^whsec_", "")), "HmacSHA256"));
        mac.update((id + "." + timestamp + ".").getBytes(StandardCharsets.UTF_8));
        byte[] expected = mac.doFinal(body);
        for (String part : signatures.split(" ")) {
            try {
                if (part.startsWith("v1,") && MessageDigest.isEqual(Base64.getDecoder().decode(part.substring(3)), expected)) {
                    return true;
                }
            } catch (IllegalArgumentException notBase64) {
                // not a signature of ours
            }
        }
        return false;
    }

    static boolean signed(HttpHeaders headers, byte[] body) throws Exception {
        return isFromTelyBot(headers.getFirst("webhook-id"), headers.getFirst("webhook-timestamp"),
                headers.getFirst("webhook-signature"), body, SECRET);
    }

    // An action: during a chat, the bot asks where an order is.
    @PostMapping("/telybot/order-status")
    ResponseEntity<Map<String, String>> orderStatus(@RequestHeader HttpHeaders headers, @RequestBody byte[] body)
            throws Exception { // the exact bytes Telybot signed
        if (!signed(headers, body)) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        JsonNode asked = json.readTree(body);
        Map<String, String> order = ORDERS.get(asked.path("order_id").asString());
        if (order == null || !order.get("email").equals(asked.path("email").asString().trim().toLowerCase(Locale.ROOT))) {
            // the bot asks the customer to check
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", "not_found"));
        }
        return ResponseEntity.ok(Map.of("status", order.get("status"), "eta", order.get("eta")));
    }

    // A webhook: something happened in a chat.
    @PostMapping("/telybot/events")
    ResponseEntity<Void> events(@RequestHeader HttpHeaders headers, @RequestBody byte[] body) throws Exception {
        if (!signed(headers, body)) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        JsonNode event = json.readTree(body);
        if (seen.add(event.path("id").asString())) { // a retry carries the same id
            System.out.println(event.path("type").asString() + " " + event.path("data")); // hand it to your CRM or a queue
        }
        return ResponseEntity.noContent().build();
    }
}
// ASP.NET Core minimal API (.NET 8 or later)
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var secret = builder.Configuration["TELYBOT_SIGNING_SECRET"]!; // whsec_…, from Actions → Security
var orders = new Dictionary<string, Order> { ["48213"] = new("asha@example.com", "shipped", "2 October") };
var seen = new ConcurrentDictionary<string, bool>(); // ids of events already handled: keep these in your database

// An action: during a chat, the bot asks where an order is.
app.MapPost("/telybot/order-status", async (HttpRequest request) =>
{
    var body = await ReadBody(request); // the exact bytes Telybot signed
    if (!IsFromTelyBot(request.Headers, body, secret)) return Results.Unauthorized();
    var asked = JsonSerializer.Deserialize<Dictionary<string, string>>(body) ?? new();
    if (!orders.TryGetValue(asked.GetValueOrDefault("order_id", ""), out var order)
        || order.Email != asked.GetValueOrDefault("email", "").Trim().ToLowerInvariant())
    {
        return Results.NotFound(new { error = "not_found" }); // the bot asks the customer to check
    }
    return Results.Ok(new { status = order.Status, eta = order.Eta });
});

// A webhook: something happened in a chat.
app.MapPost("/telybot/events", async (HttpRequest request) =>
{
    var body = await ReadBody(request);
    if (!IsFromTelyBot(request.Headers, body, secret)) return Results.Unauthorized();
    using var json = JsonDocument.Parse(body);
    var e = json.RootElement;
    if (seen.TryAdd(e.GetProperty("id").GetString()!, true)) // a retry carries the same id
    {
        Console.WriteLine($"{e.GetProperty("type").GetString()} {e.GetProperty("data")}"); // hand it to your CRM or a queue
    }
    return Results.NoContent();
});

app.Run("http://localhost:3000");

static async Task<byte[]> ReadBody(HttpRequest request)
{
    using var buffer = new MemoryStream();
    await request.Body.CopyToAsync(buffer);
    return buffer.ToArray();
}

// True when your bot signed this request in the last five minutes.
static bool IsFromTelyBot(IHeaderDictionary headers, byte[] body, string secret)
{
    string id = headers["webhook-id"].ToString(), timestamp = headers["webhook-timestamp"].ToString();
    long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    if (id.Length == 0 || !long.TryParse(timestamp, out var sent) || sent < now - 300 || sent > now + 300) return false;
    var key = Convert.FromBase64String(secret.StartsWith("whsec_") ? secret[6..] : secret);
    var signed = Encoding.UTF8.GetBytes($"{id}.{timestamp}.").Concat(body).ToArray();
    var expected = HMACSHA256.HashData(key, signed);
    foreach (var part in headers["webhook-signature"].ToString().Split(' '))
    {
        var given = new byte[32];
        if (part.StartsWith("v1,") && Convert.TryFromBase64String(part[3..], given, out var length)
            && length == given.Length && CryptographicOperations.FixedTimeEquals(given, expected))
        {
            return true;
        }
    }
    return false;
}

record Order(string Email, string Status, string Eta);
# gem install sinatra rackup puma
require 'json'
require 'openssl'
require 'sinatra'

set :port, 3000
SECRET = ENV.fetch('TELYBOT_SIGNING_SECRET') # whsec_…, from Actions → Security
ORDERS = { '48213' => { email: 'asha@example.com', status: 'shipped', eta: '2 October' } }.freeze
SEEN = Set.new # ids of events already handled: keep these in your database

# True when your bot signed this request in the last five minutes.
def from_telybot?(id, timestamp, signatures, raw_body, secret)
  return false if id.to_s.empty? || !timestamp.to_s.match?(/\A\d+\z/) || (Time.now.to_i - timestamp.to_i).abs > 300

  key = secret.delete_prefix('whsec_').unpack1('m')
  expected = [OpenSSL::HMAC.digest('SHA256', key, "#{id}.#{timestamp}.".b + raw_body.b)].pack('m0')
  signatures.to_s.split(' ').any? { |part| part.start_with?('v1,') && OpenSSL.secure_compare(part[3..], expected) }
end

before '/telybot/*' do
  @raw = request.body.read # the exact bytes Telybot signed
  halt 401 unless from_telybot?(request.env['HTTP_WEBHOOK_ID'], request.env['HTTP_WEBHOOK_TIMESTAMP'],
                                request.env['HTTP_WEBHOOK_SIGNATURE'], @raw, SECRET)
end

# An action: during a chat, the bot asks where an order is.
post '/telybot/order-status' do
  asked = JSON.parse(@raw)
  order = ORDERS[asked['order_id']]
  content_type :json
  unless order && order[:email] == asked['email'].to_s.strip.downcase
    halt 404, { error: 'not_found' }.to_json # the bot asks the customer to check
  end
  { status: order[:status], eta: order[:eta] }.to_json
end

# A webhook: something happened in a chat.
post '/telybot/events' do
  event = JSON.parse(@raw)
  if SEEN.add?(event['id']) # a retry carries the same id
    puts "#{event['type']} #{event['data']}" # hand it to your CRM or a queue
  end
  204
end

De bestellingen en de gebeurtenis-id's worden in het geheugen bewaard, zodat het voorbeeld op zichzelf draait. Bewaar ze in je database: een gebeurtenis-id dat een herstart overleeft, houdt een duplicaat nog steeds tegen.

Wat telt in je eigen code

  • Controleer de body die je hebt ontvangen, byte voor byte. Lees de ruwe body en controleer de handtekening voordat je hem parseert. JSON die is ingelezen en opnieuw weggeschreven, heeft andere witruimte en komt niet meer overeen. In Express is dat express.raw(), in Flask request.get_data(), in PHP php://input, in Spring een byte[]-body, in ASP.NET Core Request.Body en in Rails request.raw_post.
  • Vijf minuten speling, naar beide kanten. Een verzoek waarvan de timestamp verder van de klok van je server af ligt, wordt geweigerd, zodat een opgenomen verzoek later niet opnieuw kan worden verstuurd. Laat de klok van je server automatisch instellen.
  • Vergelijk in constante tijd. Elk voorbeeld vergelijkt handtekeningen met de constant-time-functie van zijn taal, zodat de duur van een controle niets verraadt over hoe dicht een gok in de buurt kwam.
  • Elk van de handtekeningen. webhook-signature kan meerdere handtekeningen bevatten, gescheiden door spaties. Het verzoek is echt als een ervan overeenkomt.
  • Antwoord snel. De bot wacht 8 seconden op een actie, en een webhook wacht er 10. Antwoord bij een webhook eerst en doe het trage werk daarna, vanuit een wachtrij.
  • Zeg "niet gevonden" altijd op dezelfde manier. Antwoord 404 zowel wanneer de bestelling niet bestaat als wanneer het e-mailadres niet overeenkomt, zodat niemand kan achterhalen welke bestelnummers echt zijn.

Bibliotheken

De handtekening volgt Standard Webhooks, waarvan de bibliotheken voor deze en andere talen de controle voor je doen: geef ze het geheim zoals het wordt getoond, inclusief whsec_, en de body precies zoals die binnenkwam.

Ingelogde klanten

Om de bot te vertellen wie een ingelogde klant is, ondertekent je server een kort token voor die klant met een ander geheim, het identiteitsgeheim. Hoe dat werkt, met de code in dezelfde talen, staat onder Geverifieerde bezoekers.

Als het niet werkt

  • Elk verzoek krijgt 401. Controleer of de server het ondertekeningsgeheim heeft, dat begint met whsec_, en niet het identiteitsgeheim, dat begint met tbid_. Nieuwe maken onder Handtekening van Telybot zet het oude geheim direct buiten werking, dus werk je server meteen daarna bij. En controleer de body voordat je hem parseert.
  • Sommige verzoeken krijgen 401. De klok van je server loopt meer dan vijf minuten verkeerd.
  • De bot zegt dat hij iets niet kon opzoeken. Acties → Alle acties laat zien waarom de laatste aanroep mislukte. Druk op Testen om er zelf een te versturen en precies te zien wat er terugkwam.
  • Een webhook is gestopt. Na vijftig mislukte leveringen op rij wordt hij gepauzeerd en krijgen de eigenaren een e-mail. Herstel de ontvanger, druk op Een testgebeurtenis verzenden en daarna op Hervatten. Leveringen toont dertig dagen lang wat je ontvanger antwoordde.
Volgende: Limieten en beveiliging →