telybot Un peu moins ordinaire.
Beaucoup plus intelligent.
FranceEnglish Connexion Contactez-nous Commencer gratuitement
Documentation TelybotRéférence

Exemples de serveur

Du code complet et fonctionnel pour les deux façons dont Telybot communique avec votre serveur, en Node.js, Python, PHP, Go, Java, C# et Ruby : une action qui répond au bot pendant une discussion, et un webhook qui est informé de ce qui s'y est passé.

Choisissez votre langage une fois : les exemples ici et dans le guide Actions le suivent.

Ce que fait l'exemple

  • POST /telybot/order-status répond à une action. Le bot envoie le numéro de commande et l'adresse e-mail saisis par le client. Le serveur répond 200 avec le statut de la commande quand les deux correspondent, et 404 sinon, pour que le bot demande au client de vérifier ce qu'il a saisi.
  • POST /telybot/events reçoit les webhooks. Une nouvelle tentative porte le même id, de sorte que chaque événement n'est traité qu'une fois, quel que soit le nombre de fois où il arrive, et la réponse est 204 immédiatement.
  • Les deux refusent, avec 401, une requête dont la signature n'est pas valide : non signée avec votre secret, modifiée en chemin ou datant de plus de cinq minutes.

Avant de commencer

  1. Ouvrez Votre bot → Actions → Sécurité et, sous Signature de Telybot, appuyez sur Afficher le secret et copiez-le. Il commence par whsec_.
  2. Donnez-le à votre serveur sous forme du paramètre TELYBOT_SIGNING_SECRET, depuis une variable d'environnement ou votre gestionnaire de secrets. Ne le mettez jamais dans une page web ou une app.
  3. Sous Actions → Nouvelle action, choisissez POST et l'adresse https://your-server.example/telybot/order-status, et demandez au client order_id (un nombre) et email (une adresse e-mail).
  4. Sous Actions → Webhooks, ajoutez https://your-server.example/telybot/events avec les événements souhaités.

Le 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

Les commandes et les identifiants d'événement sont gardés en mémoire pour que l'exemple fonctionne seul. Conservez-les dans votre base de données : un identifiant d'événement qui survit à un redémarrage continue d'empêcher un doublon.

Ce qui compte dans votre propre code

  • Vérifiez le corps reçu, octet par octet. Lisez le corps brut et vérifiez la signature avant de l'analyser. Un JSON qui a été lu puis réécrit n'a plus les mêmes espaces et ne correspond plus. En Express, c'est express.raw(), en Flask request.get_data(), en PHP php://input, en Spring un corps byte[], en ASP.NET Core Request.Body, et en Rails request.raw_post.
  • Cinq minutes dans un sens ou dans l'autre. Une requête dont l'horodatage s'écarte davantage de l'horloge de votre serveur est refusée, de sorte qu'une requête enregistrée ne puisse pas être renvoyée plus tard. Faites régler l'horloge de votre serveur automatiquement.
  • Comparez en temps constant. Chaque exemple compare les signatures avec la fonction à temps constant de son langage, de sorte que la durée d'une vérification ne révèle rien sur la proximité d'une tentative.
  • N'importe laquelle des signatures. webhook-signature peut contenir plusieurs signatures séparées par des espaces. La requête est authentique si l'une d'elles correspond.
  • Répondez vite. Le bot attend une action pendant 8 secondes, et un webhook attend 10 secondes. Pour un webhook, répondez d'abord et faites le travail lent ensuite, depuis une file d'attente.
  • Dites « introuvable » de la même façon. Répondez 404 aussi bien quand la commande n'existe pas que quand l'e-mail ne correspond pas, afin que personne ne puisse savoir quels numéros de commande sont réels.

Bibliothèques

La signature suit Standard Webhooks, dont les bibliothèques pour ces langages et d'autres la vérifient pour vous : donnez-leur le secret tel qu'il est affiché, whsec_ compris, et le corps exactement tel qu'il est arrivé.

Clients connectés

Pour indiquer au bot qui est un client connecté, votre serveur signe pour lui un jeton court avec un autre secret, le secret d'identité. La marche à suivre, avec le code dans les mêmes langages, se trouve sous Visiteurs vérifiés.

Quand cela ne fonctionne pas

  • Chaque requête reçoit 401. Vérifiez que le serveur a le secret de signature, qui commence par whsec_, et non le secret d'identité, qui commence par tbid_. En créer un nouveau sous Signature de Telybot désactive immédiatement l'ancien secret : mettez donc à jour votre serveur juste après. Et vérifiez le corps avant de l'analyser.
  • Certaines requêtes reçoivent 401. L'horloge de votre serveur est décalée de plus de cinq minutes.
  • Le bot dit qu'il n'a pas pu trouver une information. Actions → Toutes les actions indique pourquoi le dernier appel a échoué. Appuyez sur Tester pour en envoyer un vous-même et voir exactement ce qui est revenu.
  • Un webhook s'est arrêté. Après cinquante livraisons échouées d'affilée, il est mis en pause et les propriétaires reçoivent un e-mail. Corrigez le récepteur, appuyez sur Envoyer un événement de test, puis sur Reprendre. Livraisons liste ce que votre récepteur a répondu, pendant trente jours.
Suivant: Limites et sécurité →