talkieback

API serveur et webhooks

Depuis votre serveur : lire les retours, répondre, changer un statut, supprimer un utilisateur. Et recevoir chaque événement, signé, à l'adresse de votre choix.

Authentification

Toutes les adresses commencent par https://talkieback.com/api/server/v1. Chaque appel porte le secret de l'app dans l'en-tête Authorization. Le secret commence par sk_live_ : il se crée sur la page « Identité vérifiée » ou dans Réglages, Pour les développeurs, et ne va que sur votre serveur, jamais dans l'app.

Terminal
curl "https://talkieback.com/api/server/v1/feedbacks?status=new&limit=20" \
  -H "Authorization: Bearer sk_live_…"

Les réponses sont en JSON. Un secret régénéré laisse l'ancien valable 24 h, le temps de mettre à jour votre serveur.

Limites

60 requêtes par minute et par app, et un quota par jour qui dépend de l'offre. Au-delà, la réponse est 429 : réessayez un peu plus tard. Supprimer un utilisateur n'est jamais compté.

Requêtes par jour selon l'offre
OffreRequêtes par jour
Gratuit1 000
Solo5 000
Pro100 000
Business500 000

Les retours

GET/feedbacks

Les retours de l'app, du plus récent au plus ancien.

ChampCe qu'il fait
statusFacultatif. Un statut : new, planned, in_progress, done, wont, spam.
limitFacultatif. De 1 à 100 retours par page, 50 par défaut.
cursorFacultatif. La valeur next_cursor de la page précédente.
Réponse
{
  "data": [ { "id": "3f6d2a9e-8c1b-4f5e-9a7d-2b4c6e8f1a3d", "kind": "voice", "status": "new", … } ],
  "next_cursor": "eyJjIjoiMjAyNi0xMC0yOVQxNDowMjoxMS4wMDBaIn0"
}

next_cursor vaut null à la dernière page.

GET/feedbacks/:id

Un retour, avec son texte (ou sa transcription), la page, l'appareil et l'utilisateur.

Réponse
{
  "id": "3f6d2a9e-8c1b-4f5e-9a7d-2b4c6e8f1a3d",
  "app_id": "8b0e5c2d-41f7-4c3a-b6e9-7d1f0a2c5e84",
  "kind": "voice",
  "text": "Le bouton pour échanger une carte ne répond plus sur l'album Axis.",
  "route": "/album/axis",
  "status": "new",
  "reward": null,
  "tags": [],
  "test": false,
  "created_at": "2026-10-29T14:02:11.000Z",
  "resolved_at": null,
  "platform": "web",
  "app_version": null,
  "context": { "title": "Album Axis", "lang": "fr-FR" },
  "user": {
    "id": "c4a1e7b2-9d3f-4e6a-8b5c-1f2e3d4c5b6a",
    "external_id": "42",
    "verified": true,
    "email": "camille@exemple.fr",
    "name": "Camille"
  }
}

POST/feedbacks/:id/replies

Répondre à l'utilisateur. Talkieback le prévient dans le widget, par notification ou par e-mail.

ChampCe qu'il fait
bodyLe message. Facultatif si status change seul.
statusFacultatif. Le nouveau statut du retour.
rewardFacultatif, au passage à « done » ou sur un retour déjà fait. La récompense montrée à l'utilisateur, transmise dans feedback.rewarded.
internalFacultatif. true : une note interne, que l'utilisateur ne voit pas.
Terminal
curl -X POST "https://talkieback.com/api/server/v1/feedbacks/3f6d2a9e-8c1b-4f5e-9a7d-2b4c6e8f1a3d/replies" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"body": "Corrigé dans la version 2.4. Merci Camille.", "status": "done", "reward": "5 boosters"}'

PATCH/feedbacks/:id

Changer le statut, les étiquettes ou la personne qui suit le retour, sans écrire à l'utilisateur.

ChampCe qu'il fait
statusFacultatif. Le nouveau statut.
tagsFacultatif. La liste complète des étiquettes, par exemple ["album", "bug"].
assigneeFacultatif. L'e-mail d'un membre de l'équipe, ou null.
Terminal
curl -X PATCH "https://talkieback.com/api/server/v1/feedbacks/3f6d2a9e-8c1b-4f5e-9a7d-2b4c6e8f1a3d" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"status": "planned", "tags": ["album"]}'

Les utilisateurs

DELETE/users/:externalId

Supprimer un utilisateur et tous ses retours (RGPD). Jamais compté dans le quota.

externalId est l'identifiant que votre app passe à identify. Les retours, les messages et l'audio sont effacés, sans retour possible.

Terminal
curl -X DELETE "https://talkieback.com/api/server/v1/users/42" \
  -H "Authorization: Bearer sk_live_…"
Réponse
{ "deleted": 3 }

Les erreurs : 401 secret absent ou refusé, 404 introuvable, 422 champ invalide (le message dit lequel), 429 trop de requêtes.

Webhooks

Ajoutez une adresse dans Réglages, Webhooks, et choisissez les événements. Talkieback y envoie un POST en JSON à chaque événement. Répondez 2xx en moins de 10 secondes ; sinon, Talkieback réessaie après 1 min, 5 min, 30 min, 2 h, 6 h et 12 h, pendant 24 h.

Les événements
ÉvénementQuand
feedback.createdUn retour arrive.
feedback.status_changedVous changez le statut d'un retour.
feedback.resolvedUn retour passe à « Fait » pour la première fois. Envoyé une seule fois par retour, avec la récompense.
feedback.rewardedVotre équipe donne une récompense, au passage à « Fait » ou plus tard. Envoyé à chaque récompense : écoutez-le pour la créditer.
feedback.repliedVotre équipe répond à un utilisateur.
feedback.user_repliedUn utilisateur répond dans le fil.

Chaque envoi a la même forme : id (unique : un événement qui revient a le même), type, created_at et data.

feedback.rewarded
{
  "id": "evt_8Kq2mXz4Tb7Rn3Wc9P",
  "type": "feedback.rewarded",
  "created_at": "2026-10-29T15:10:42.000Z",
  "data": {
    "feedback": { "id": "3f6d2a9e-8c1b-4f5e-9a7d-2b4c6e8f1a3d", "status": "done", "user": { "external_id": "42", "verified": true }, … },
    "reward": "5 boosters"
  }
}

Vérifier la signature

Chaque envoi porte l'en-tête Talkieback-Signature: t=1761750642,v1=5f2b…. v1 est HMAC-SHA256(secret du webhook, t.corps) en hexadécimal, où t est l'heure d'envoi en secondes et corps le corps brut de la requête. Refusez une signature fausse, ou vieille de plus de 5 minutes.

Node (Express)
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();

// le corps brut : la signature porte sur les octets reçus, pas sur le JSON relu
app.post('/webhooks/talkieback', express.raw({ type: 'application/json' }), (req, res) => {
  const parts = Object.fromEntries(
    (req.get('Talkieback-Signature') ?? '').split(',').map((p) => p.split('=')),
  );
  const expected = createHmac('sha256', process.env.TALKIEBACK_WEBHOOK_SECRET)
    .update(`${parts.t}.${req.body}`)
    .digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const valid =
    typeof parts.v1 === 'string' &&
    parts.v1.length === expected.length &&
    timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!fresh || !valid) return res.status(400).send('Signature invalide');

  const event = JSON.parse(req.body);
  // event.id est unique : ignorez un événement déjà traité
  if (event.type === 'feedback.rewarded') {
    // la récompense à créditer : event.data.reward
  }
  res.sendStatus(200);
});

app.listen(3000);
Python (Flask)
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)


@app.post("/webhooks/talkieback")
def talkieback_webhook():
    header = request.headers.get("Talkieback-Signature", "")
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    body = request.get_data()
    expected = hmac.new(
        os.environ["TALKIEBACK_WEBHOOK_SECRET"].encode(),
        parts.get("t", "").encode() + b"." + body,
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(expected, parts.get("v1", "")):
        abort(400)
    if abs(time.time() - int(parts["t"])) > 300:
        abort(400)
    event = request.get_json()
    # event["id"] est unique : ignorez un événement déjà traité
    return "", 200
PHP
<?php
$secret = getenv('TALKIEBACK_WEBHOOK_SECRET');
$body = file_get_contents('php://input');

$parts = [];
foreach (explode(',', $_SERVER['HTTP_TALKIEBACK_SIGNATURE'] ?? '') as $part) {
    [$key, $value] = array_pad(explode('=', $part, 2), 2, '');
    $parts[$key] = $value;
}

$expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $body, $secret);
$fresh = abs(time() - (int) ($parts['t'] ?? 0)) < 300;
if (!$fresh || !hash_equals($expected, $parts['v1'] ?? '')) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);
// $event['id'] est unique : ignorez un événement déjà traité
http_response_code(200);

Identité vérifiée

Offres Pro et Business. Votre serveur signe l'identifiant de chaque utilisateur connecté : hash = HMAC-SHA256(secret de l'app, id), en hexadécimal, sur l'identifiant en texte. Puis il passe l'identifiant et le hash au bouton. L'utilisateur retrouve ses retours sur tous ses appareils, et personne ne peut se faire passer pour un autre.

Node
import { createHmac } from 'node:crypto';

const hash = createHmac('sha256', process.env.TALKIEBACK_SECRET)
  .update(String(user.id))
  .digest('hex');
Python
import hashlib
import hmac
import os

user_hash = hmac.new(
    os.environ["TALKIEBACK_SECRET"].encode(),
    str(user.id).encode(),
    hashlib.sha256,
).hexdigest()
PHP
<?php
$hash = hash_hmac('sha256', (string) $user->id, getenv('TALKIEBACK_SECRET'));
supabase/functions/talkieback-hash/index.ts
// supabase functions deploy talkieback-hash
// supabase secrets set TALKIEBACK_SECRET=sk_live_…
import { createClient } from 'npm:@supabase/supabase-js@2';

const encoder = new TextEncoder();

async function hmacHex(secret: string, value: string): Promise<string> {
  const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
  const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(value));
  return Array.from(new Uint8Array(signature), (b) => b.toString(16).padStart(2, '0')).join('');
}

const cors = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') return new Response('ok', { headers: cors });
  // l'utilisateur connecté, d'après son jeton Supabase : jamais un id envoyé par le navigateur
  const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!, {
    global: { headers: { Authorization: req.headers.get('Authorization') ?? '' } },
  });
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return new Response('Unauthorized', { status: 401, headers: cors });
  const hash = await hmacHex(Deno.env.get('TALKIEBACK_SECRET')!, user.id);
  return Response.json({ id: user.id, hash }, { headers: cors });
});
HTML
<script>
  window.talkieback = window.talkieback || [];
  talkieback.push(['identify', { id: '42', email: 'camille@exemple.fr', name: 'Camille' }, '<hash calculé par le serveur>']);
</script>

Langue

Le bouton parle français, anglais, allemand, italien, espagnol, néerlandais, portugais (Portugal et Brésil), tchèque, slovaque, polonais, hongrois, turc, russe, chinois, japonais et coréen. Avec la langue « Celle de l'utilisateur » (Réglages, Apparence), il prend la langue que vous lui donnez. Si vous n'en donnez pas, ou si elle n'est pas proposée, il prend celle de la page (<html lang>), puis celles du navigateur, et l'anglais si aucune n'est proposée. L'utilisateur peut la changer en bas du panneau.

Si votre app connaît la langue du compte, passez-la, avant ou après le chargement, ou dans identify (champ lang ou locale). La voix est transcrite dans la langue parlée, quelle que soit celle du panneau.

HTML
<script>
  window.talkieback = window.talkieback || [];
  talkieback.push(['lang', 'de']);
</script>

Notifications

Sur un site, un navigateur n'accepte les notifications que d'un service worker de votre domaine. Ajoutez cette ligne à votre service worker, ou créez le fichier /talkieback-sw.js à la racine du site avec cette seule ligne :

Service worker
importScripts('https://cdn.talkieback.com/sw.js');

Dans une app native, déposez votre jeton d'accès Expo, ou vos clés FCM et APNs, dans Réglages, Notifications. Le paquet enregistre l'appareil tout seul. Si votre app a déjà ses notifications, envoyez-les vous-même à la réception de feedback.replied.

Une étape bloque ? Écrivez-nous.