Messages back
TeliTask can tell your system when a call or a round finishes, so you never have to keep asking. Give it one web address; it posts a signed message there.
Set it up
In TeliTask, open Settings, then Connections, then Send results to your system. Enter an https:// address on your server and choose which messages to receive. The screen shows your signing secret (whsec_…): keep it on your server. “Send a test” posts a test message so you can check your end before a real call.
What arrives
call.finished: a call ended for good. It was answered, nobody picked up after the last try, or it could not be placed. If the person asked to be called back,data.callbacksays when.round.finished: every call in a round has ended, with the totals.
POST https://shop.example.com/telitask-hook
Content-Type: application/json
X-TeliTask-Event: call.finished
X-TeliTask-Timestamp: 1791024409
X-TeliTask-Signature: sha256=5f2a…
{
"id": "0b8e6c1d-5f43-4a7e-8d1c-9a2f6b3e7c10",
"type": "call.finished",
"created_at": "2026-10-03T11:33:41.000Z",
"data": { …the same body as GET /api/v1/calls/{id}… }
}{
"id": "…",
"type": "round.finished",
"created_at": "2026-10-06T15:00:02.000Z",
"data": {
"id": "…",
"label": "Returns, 6 Oct",
"status": "completed",
"script": "returned-orders",
"created_at": "2026-10-06T06:00:00.000Z",
"completed_at": "2026-10-06T15:00:00.000Z",
"totals": { "calls": 412, "done": 351, "no_answer": 51, "failed": 6,
"cancelled": 4, "calling": 0, "waiting": 0 }
}
}data in call.finished is exactly what GET /api/v1/calls/{id} returns, so one piece of code can read both.
Check the signature
Anyone can post to your address, so check every message came from TeliTask before you act on it. The signature is an HMAC-SHA256, made with your signing secret, of the timestamp, a full stop, and the raw body: <timestamp>.<body>. Compare it with the X-TeliTask-Signature header (after sha256=), and refuse a timestamp more than five minutes old so an old message cannot be replayed.
Read the body as raw bytes. Parsing and re-encoding the JSON changes it and breaks the check.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = express();
const secret = mySecrets.telitaskSigningSecret; // whsec_…, from Settings, kept on your server
// Read the body raw: the signature covers the exact bytes we sent.
app.post('/telitask-hook', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-TeliTask-Timestamp') ?? '';
const signature = req.get('X-TeliTask-Signature') ?? '';
const body = req.body.toString('utf8');
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
const expected = 'sha256=' +
createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
const ok = age <= 300 &&
expected.length === signature.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!ok) return res.status(401).end();
const message = JSON.parse(body);
// message.id is unique per message: skip one you have already handled.
res.status(204).end();
handle(message); // after answering, so a slow job never makes us retry
});import hashlib, hmac, json, os, time
from flask import Flask, request
app = Flask(__name__)
SECRET = my_secrets["telitask_signing_secret"].encode() # whsec_…, kept on your server
@app.post("/telitask-hook")
def telitask_hook():
timestamp = request.headers.get("X-TeliTask-Timestamp", "")
signature = request.headers.get("X-TeliTask-Signature", "")
body = request.get_data() # raw bytes, exactly as sent
expected = "sha256=" + hmac.new(
SECRET, timestamp.encode() + b"." + body, hashlib.sha256
).hexdigest()
fresh = timestamp.isdigit() and abs(time.time() - int(timestamp)) <= 300
if not (fresh and hmac.compare_digest(expected, signature)):
return "", 401
message = json.loads(body)
# message["id"] is unique per message: skip one you have already handled.
return "", 204Answering, and retries
- Answer with any 2xx status within 8 seconds. Do slow work after you answer.
- If your address does not answer 2xx, TeliTask tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, then every 6 hours, for 24 hours from the first try.
- Each try is signed again with a new timestamp.
idstays the same, so use it to skip a message you already handled. - Redirects are not followed. Point the address at the final URL.
- Settings lists the last sends and whether they worked, and “Retry” re-posts one by hand.