From your server: read feedback, reply, change a status, delete a user. And receive every event, signed, at the address of your choice.
Authentication
Every address starts with https://talkieback.com/api/server/v1. Each call carries the app secret in the Authorization header. The secret starts with sk_live_: you create it on the “Verified identity” page or in Settings, For developers, and it only goes on your server, never in the app.
Responses are in JSON. When you regenerate a secret, the old one stays valid for 24 h, giving you time to update your server.
Limits
60 requests per minute per app, and a daily quota that depends on the plan. Beyond that, the response is 429: try again a little later. Deleting a user never counts.
Requests per day by plan
Plan
Requests per day
Free
1,000
Solo
5,000
Pro
100,000
Business
500,000
Feedback
GET/feedbacks
The feedback of the app, newest first.
Field
What it does
status
Optional. A status: new, planned, in_progress, done, wont, spam.
limit
Optional. From 1 to 100 items per page, 50 by default.
cursor
Optional. The next_cursor value of the previous page.
Errors: 401 secret missing or refused, 404 not found, 422 invalid field (the message says which), 429 too many requests.
Webhooks
Add an address in Settings, Webhooks, and choose the events. Talkieback sends a JSON POST to it for each event. Respond with 2xx within 10 seconds; otherwise, Talkieback retries after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h, for 24 h.
Events
Event
When
feedback.created
New feedback arrives.
feedback.status_changed
You change the status of a piece of feedback.
feedback.resolved
Feedback moves to “Done” for the first time. Sent only once per piece of feedback, with the reward.
feedback.rewarded
Your team gives a reward, when moving to “Done” or later. Sent for each reward: listen to it to credit the reward.
feedback.replied
Your team replies to a user.
feedback.user_replied
A user replies in the thread.
Each delivery has the same shape: id (unique: an event that comes back has the same one), type, created_at and data.
Each delivery carries the header Talkieback-Signature: t=1761750642,v1=5f2b…. v1 is HMAC-SHA256(webhook secret, t.body) in hexadecimal, where t is the send time in seconds and body the raw request body. Reject a wrong signature, or one older than 5 minutes.
Node (Express)
import { createHmac, timingSafeEqual } from'node:crypto';
import express from'express';
const app = express();
// the raw body: the signature covers the bytes received, not the parsed JSON
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('Invalid signature');
const event = JSON.parse(req.body);
// event.id is unique: ignore an event you already handledif (event.type === 'feedback.rewarded') {
// the reward to credit: 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"] is unique: ignore an event you already handled
return"", 200
Pro and Business plans. Your server signs the ID of each signed-in user: hash = HMAC-SHA256(app secret, id), in hexadecimal, on the ID as text. Then it passes the ID and the hash to the button. Users find their feedback on all their devices, and no one can pretend to be someone else.
The button speaks French, English, German, Italian, Spanish, Dutch, Portuguese (Portugal and Brazil), Czech, Slovak, Polish, Hungarian, Turkish, Russian, Chinese, Japanese and Korean. With the language set to “The user’s” (Settings, Appearance), it uses the language you give it. If you don’t give one, or if it isn’t offered, it uses the language of the page (<html lang>), then those of the browser, and English if none is offered. Users can change it at the bottom of the panel.
If your app knows the account language, pass it, before or after loading, or in identify (lang or locale field). Voice is transcribed in the spoken language, whatever the panel language.
On a website, a browser only accepts notifications from a service worker on your domain. Add this line to your service worker, or create the file /talkieback-sw.js at the root of your site with just this line:
In a native app, add your Expo access token, or your FCM and APNs keys, in Settings, Notifications. The package registers the device on its own. If your app already has its own notifications, send them yourself when you receive feedback.replied.