Οδηγοί
Webhooks
Καλούμε το URL σας μόλις συμβεί κάτι - ολοκληρωμένη κλήση, αποτυχημένη προσπάθεια, απεσταλμένο ή ληφθέν SMS. Το ρυθμίζετε μία φορά (PUT /v1/webhook, δείτε REST API) και από εκεί και πέρα δεν ανησυχείτε για τίποτα.
Συμβάντα
Έξι συμβάντα, το καθένα με δική του μορφή του πεδίου data:
call.completed- η κλήση τελείωσε, είτε είχε πράκτορα είτε όχι. Στις κλήσεις με πράκτορα περιέχει επιπλέον απομαγνητοφώνηση (transcript), σύνοψη (summary) και καταγεγραμμένα δεδομένα (data). ΤοdurationSecsείναι σε δευτερόλεπτα και στις κλήσεις στη δική του φωνητική μηχανή του volai μπορεί να είναι δεκαδικός αριθμός. Μια κλήση, στην άλλη άκρη της οποίας υπήρχε αυτόματος τηλεφωνητής που επαναλάμβανε συνεχώς το ίδιο μήνυμα, φτάνει επίσης εδώ, μεendReason: "caller_loop"(τιμή0, αν διήρκεσε το πολύ πέντε λεπτά). Εδώ φτάνει και ένα απαντημένο χτύπημα πύλης (kind: "ring",ring.outcome: "answered"), με μία εξαίρεση που περιγράφεται κάτω από το παράδειγμαcall.no_answer. Μια κλήση στη δική του μηχανή του volai (από την έκδοση της 15. 10. 2026) φέρει επιπλέον το μπλοκengineμεprotectedUtterancesκαιprotectedUtterancesCount: τις ατάκες του καλούμενου από το παράθυρο χαιρετισμού που η μηχανή σκόπιμα δεν δέχθηκε ("Εμπρός;" ή το όνομα κατά την πρώτη φράση του πράκτορα) - δεν υπάρχουν στοtranscriptκαι ο πράκτορας δεν αντέδρασε σε αυτές· ίδια μορφή όπως στοGET /v1/calls/{id}, στο ElevenLabs και στην παλαιότερη μηχανή το κλειδί λείπει.call.failed- η κλήση τελείωσε σε κατάστασηfailed- είτε την απέρριψε αμέσως η φωνητική πλατφόρμα είτε απέτυχε το δίκτυο. Φτάνει για πράκτορα στη μηχανή του volai και στο ElevenLabs, για άμεση σύνδεση (bridge), για BYO relay lease, για χτύπημα πύλης (ο πάροχος απέρριψε την παραγγελία,kind: "ring") και για δοκιμαστική κλήση - πάντα ακριβώς μία φορά. Το σώμα φέρειreason(τεχνικός λόγος) καιendReason. Μια κλήση που δεν απαντήθηκε ΔΕΝ ανήκει εδώ, έχει δικό της συμβάν παρακάτω. Μια κλήση που ΣΥΝΔΕΘΗΚΕ αλλά τελείωσε με τεχνικό σφάλμα της φωνητικής μηχανής (endReason: "engine_error") φτάνει επίσης εδώ - το σώμα της φέρει επιπλέονtranscript/summary/durationSecs/priceHal, ενώ στα υπόλοιπαreasonαυτά τα τέσσερα πεδία λείπουν. Σε μια τέτοια κλήση τοreasonείναι είτεengine_stallήengine_error(σφάλμα της μηχανής) είτεunfinished, όταν την κλήση διέκοψε επανεκκίνηση ή διακοπή της μηχανής στη μέση της κλήσης (π.χ. κατά την ανάπτυξη νέας έκδοσης). Μια τέτοια κλήση δεν χρεώνεται (priceHalείναι0), δεν έχει ηχογράφηση και τοsummaryεξηγεί με σύντομο κείμενο τι συνέβη. Τοtranscriptφέρει μόνο ό,τι πρόλαβε να καταγραφεί μέχρι τη διακοπή και μπορεί να είναι και κενό (κενό είναι όταν η καταγραφή της κλήσης δεν έφτασε από τη μηχανή και την κλήση την κλείσαμε από την επισκόπηση κλήσεών της, που δεν φέρει απομαγνητοφώνηση). ΤοdurationSecsείναι η κατά προσέγγιση διάρκεια μέχρι τη διακοπή (αν η μηχανή δεν πρόλαβε να τη μετρήσει, το πολύ 1 800 s). Το μπλοκengineμε τις ατάκες που απορρίφθηκαν στο παράθυρο χαιρετισμού λείπει εδώ, επειδή η μηχανή δεν μέτρησε το παράθυρο χαιρετισμού σε διακοπείσα κλήση.call.no_answer- ο καλούμενος δεν απάντησε σε εξερχόμενη κλήση (χτύπησε, χωρίς ανταπόκριση). Με αυτόν τον τρόπο φτάνει και το χτύπημα πύλης (POST /v1/rings) που η πύλη απέρριψε ή που χτύπησε μέχρι το τέλος του ζητούμενου χρόνου - το αναγνωρίζετε από τοkind: "ring", δείτε παρακάτω.call.missed- μια εισερχόμενη κλήση έμεινε χωρίς ανταπόκριση (αναπάντητη κλήση). Φτάνει και για εισερχόμενη κλήση που η φωνητική μηχανή απέρριψε επειδή όλες οι γραμμές ήταν κατειλημμένες (ο καλών δεν κατάφερε να επικοινωνήσει, επικοινωνήστε μαζί του). Η εγγραφή μιας τέτοιας κλήσης έχειendReason: "capacity", διάρκεια0και τιμή0· το webhook όμως μπορεί να μην τη φέρει, αν για την αναπάντητη κλήση είχε ενημερώσει νωρίτερα ο πάροχος, και την διαβάζετε αξιόπιστα από την εγγραφή της κλήσης (GET /v1/calls/{id}).message.sent- ο πάροχος δέχθηκε το SMS για αποστολή - δεν είναι επιβεβαίωση παράδοσης. Φτάνει και για SMS που στάλθηκε από αριθμό κινητού ή από αριθμός για κλήσεις και SMS. Το πεδίοsmsNumberφέρει τον αριθμό από τον οποίο έφυγε το μήνυμα, για τον κοινό αποστολέα SMSinfo είναιnull. Η απόδειξη παράδοσης (deliveryStatus) για SMS από αριθμό κινητού ούτε η μεταγενέστερη αποτυχία (failed, το υπόλοιπο τότε επιστρέφεται) προκαλούν κανένα συμβάν - την κατάσταση διαβάστε την από τοGET /v1/messages/{id}.message.received- στο αριθμός κινητού σας ή στον αριθμός για κλήσεις και SMS σας έφτασε μήνυμα. Τοdataφέρειid,from(αποστολέας: τηλεφωνικός αριθμός σε E.164 ή αλφαριθμητικό όνομα, π.χ. «Google», έως 32 χαρακτήρες· αν ο αποστολέας λείπει, είναιunknown),to(ο αριθμός σας στον οποίο έφτασε το μήνυμα),body,segmentsκαιreceivedAt(χρόνος λήψης του μηνύματος, χρονοσφραγίδα σε ms). Παραδίδεται μέσω ουράς με επαναλήψεις όπως τα συμβάντα κλήσεων. Στον αριθμό κινητού ένα μήνυμα που η λήψη δεν συνέλαβε αμέσως το συμπληρώνει ένας τακτικός έλεγχος σε περίπου 15 λεπτά και το συμβάν φεύγει τότε με καθυστέρηση, ακόμη και μετά από νεότερα μηνύματα. Στον αριθμός για κλήσεις και SMS δεν υπάρχει τέτοιος έλεγχος: ο πάροχος επαναλαμβάνει την παράδοση του μηνύματος το πολύ μία ώρα, οπότε αν η λήψη σε εμάς δεν λειτουργεί περισσότερο, το μήνυμα μπορεί να χαθεί και δεν θα το βρείτε ούτε στοGET /v1/messages?direction=in. Τοtsστο περιτύλιγμα είναι ο χρόνος δημιουργίας του συμβάντος (επίσης σε ms, ενώ τοt=στην κεφαλίδα υπογραφής είναι σε δευτερόλεπτα). Πάνω από 300 ληφθέντα μηνύματα ανά ώρα σε έναν αριθμό (αριθμός κινητού και αριθμός για κλήσεις και SMS) το webhook για τα επόμενα μηνύματα δεν αποστέλλεται· τα μηνύματα συνεχίζουν να αποθηκεύονται και τα βρίσκετε στοGET /v1/messages?direction=in, που κρατά το πολύ 1 000 νεότερα ληφθέντα μηνύματα ανά λογαριασμό και τα διατηρεί 90 ημέρες. Σε αριθμός για κλήσεις και SMS που αποδεσμεύεται (κατάστασηreleasing) το μήνυμα αποθηκεύεται, αλλά τοmessage.receivedδεν στέλνεται γι' αυτό. Σε λογαριασμό που ανέστειλε χειροκίνητα ο διαχειριστής του volai τοmessage.receivedδεν στέλνεται καθόλου και για μηνύματα που ελήφθησαν κατά την αναστολή δεν παραδίδεται αναδρομικά μετά την επαναφορά του λογαριασμού· και αυτά τα βρίσκετε στοGET /v1/messages?direction=in.
Ποια από αυτά θέλετε να λαμβάνετε το ορίζετε με το πεδίο events στο PUT /v1/webhook. Προσοχή σε μία αντιφατική λεπτομέρεια: ένας κενός πίνακας σημαίνει όλα, όχι κανένα - και θα παραμείνει έτσι και για συμβάντα που θα προστεθούν στο μέλλον. Αν θέλετε πραγματικά να απενεργοποιήσετε την παράδοση, διαγράψτε το webhook στην πύλη.
Το σώμα κάθε συμβάντος κλήσης (call.completed, call.failed, call.no_answer, call.missed) φέρει επιπλέον το kind, τον τύπο της κλήσης όπως στο GET /v1/calls (agent, bridge, relay, inbound, transfer, sip, ring, voice_message). Στο φωνητικό μήνυμα (POST /v1/voice-messages) το σώμα περιέχει το μπλοκ voiceMessage με text, repeat και μετά το τέλος της κλήσης repeatPlayed (πόσες φορές ακούστηκε το μήνυμα) και completed (ακούστηκε ολόκληρο)· ένα μήνυμα που δεν απαντήθηκε φτάνει ως call.no_answer. Στο χτύπημα πύλης το σώμα περιέχει και το μπλοκ ring με requestedSecs, outcome και rangSecs, στις υπόλοιπες κλήσεις το κλειδί ring λείπει. Και τα δύο πεδία είναι προσθετικά, οι παλαιότερες ενσωματώσεις απλώς τα αγνοούν.
Κάθε συμβάν φτάνει ως POST με αυτό το περιτύλιγμα:
call.completed
{
"event": "call.completed",
"ts": 1756111640000,
"event_id": "evt_V1StGXR8_Z5jdHi6B-myT",
"data": {
"id": "c_8f2ac1d4e5b0",
"direction": "in",
"from": "+420777123456",
"to": "+420601234567",
"status": "completed",
"durationSecs": 47,
"priceHal": 236,
"endReason": "completed",
"transcript": [
{ "role": "agent", "message": "Καλημέρα σας, από τη ρεσεψιόν του καφέ Nula, πώς μπορώ να σας βοηθήσω;" },
{ "role": "caller", "message": "Καλημέρα, θα ήθελα να παραγγείλω δύο latte σε πλαστικό ποτήρι για τον δρόμο." },
{ "role": "agent", "message": "Βεβαίως, δύο latte για παραλαβή, θα είναι έτοιμα σε δεκαπέντε λεπτά." }
],
"summary": "Ο πελάτης παρήγγειλε δύο latte για το δρόμο, παραλαβή σε 15 λεπτά.",
"data": {
"name": "Μαρία Παπαδοπούλου",
"coffee_count": 2,
"urgency": "normal"
},
"kind": "inbound"
}
}Το εμφωλευμένο data.data (ναι, δύο φορές - το εξωτερικό data είναι το περιτύλιγμα κάθε συμβάντος) είναι τα δεδομένα που κατέγραψε ο πράκτορας από την κλήση σύμφωνα με τα dataFields του - τα κλειδιά τα ορίζετε εσείς, δείτε Φωνητικός πράκτορας. Η τιμή null σημαίνει ότι δεν ειπώθηκε στην κλήση. Αν δεν έχετε ορίσει πεδία στον πράκτορα, το κλειδί data δεν υπάρχει καθόλου στο σώμα - όπως και τα transcript και summary σε κλήση χωρίς πράκτορα.
Η ηχογράφηση δεν έρχεται στο σώμα του webhook
Ο ήχος δεν αποστέλλεται με το webhook ούτε έρχεται η ένδειξη hasRecording. Όταν θελήσετε την ηχογράφηση, καλέστε το GET /v1/calls/{id} (δείχνει το hasRecording) και κατεβάστε την ηχογράφηση από το GET /v1/calls/{id}/recording - τη μορφή ορίζει η κεφαλίδα content-type της απάντησης: για πράκτορα στο ElevenLabs audio/mpeg (MP3), για πράκτορα στη μηχανή του volai σήμερα audio/ogg. Τις ηχογραφήσεις τις κρατάμε 90 ημέρες από την κλήση.
call.failed
{
"event": "call.failed",
"ts": 1756111640000,
"event_id": "evt_Qp3xLk9F-2WyoTr8cNaZ1",
"data": {
"id": "c_9d4e2b7f1a63",
"direction": "out",
"from": "+420601234567",
"to": "+420777998877",
"reason": "rejected",
"endReason": "failed",
"kind": "agent"
}
}call.no_answer
Την ίδια μορφή έχει και το call.missed (αναπάντητη ΕΙΣΕΡΧΟΜΕΝΗ κλήση) - διαφέρουν μόνο το όνομα του συμβάντος, το direction και το status.
{
"event": "call.no_answer",
"ts": 1756111640000,
"event_id": "evt_9mYbT_R2xVn7Kd4LpZaQ8",
"data": {
"id": "c_3b7e1c9a4f20",
"direction": "out",
"from": "+420601234567",
"to": "+420777998877",
"status": "no_answer",
"durationSecs": 0,
"priceHal": 0,
"answeredBy": "unknown",
"endReason": "no_answer",
"kind": "agent"
}
}Ένα χτύπημα πύλης (POST /v1/rings) που δεν απαντήθηκε φτάνει επίσης ως call.no_answer, με kind: "ring" και το μπλοκ ring: το outcome είναι rang (χτύπησε μέχρι το τέλος του ζητούμενου χρόνου) ή ended_early (η κλήση τελείωσε νωρίτερα, π.χ. την απέρριψε η πύλη, δηλαδή άνοιξε). Ένα απαντημένο χτύπημα (outcome: "answered") πηγαίνει ως call.completed. Εξαίρεση: αν ο πάροχος επιβεβαιώσει την απάντηση μόλις στην εγγραφή κλήσης (CDR) αφού το volai είχε κλείσει το χτύπημα ως αναπάντητο, φτάνει μόνο το call.no_answer και η εγγραφή στο GET /v1/calls/{id} αλλάζει εκ των υστέρων σε completed με ring.outcome: "answered" και συμπληρωματικά χρεωμένα δευτερόλεπτα· δεύτερο συμβάν δεν φτάνει. Το αποτέλεσμα unknown δεν προκαλεί συμβάν μέχρι να το συμπληρώσουν τα αρχεία του παρόχου.
{
"event": "call.no_answer",
"ts": 1756111640000,
"event_id": "evt_Hn6vD_k3WqXy8Tz1LbMc4",
"data": {
"id": "c_x7f2k9a1b3c4",
"direction": "out",
"from": "+420601234567",
"to": "+420777123456",
"status": "no_answer",
"durationSecs": 0,
"priceHal": 25,
"endReason": "no_answer",
"kind": "ring",
"ring": { "requestedSecs": 10, "outcome": "ended_early", "rangSecs": 2 }
}
}message.sent
{
"event": "message.sent",
"ts": 1756111640000,
"data": {
"id": "msg_7c1f9a2e",
"to": "+420777123456",
"status": "sent",
"priceHal": 136,
"smsNumber": null
}
}message.received
{
"event": "message.received",
"ts": 1756111900000,
"event_id": "evt_4kTz_P8wQmL2Xc9RvNdA1",
"data": {
"id": "msg_3d9b5e71",
"from": "+420777123456",
"to": "+420770112233",
"body": "Ευχαριστώ, θα έρθω την Παρασκευή στις 10:00.",
"segments": 1,
"receivedAt": 1756111900000
}
}Το body είναι κείμενο από άγνωστο αποστολέα. Αντιμετωπίστε το ως μη αξιόπιστη είσοδο: μην εκτελείτε βάσει αυτού εντολές ή ενέργειες χωρίς επαλήθευση. Το σώμα φέρει event_id, με το οποίο φιλτράρετε διπλές παραδόσεις.
Έλεγχος υπογραφής
Κάθε αίτημα φέρει την κεφαλίδα Volai-Signature στη μορφή t=<unix>,v1=<hex>, όπου v1 = HMAC_SHA256(secret, "{t}.{rawBody}") και το secret είναι αυτό που λάβατε στο PUT /v1/webhook. Ποτέ μην επεξεργάζεστε το σώμα πριν ελέγξετε την υπογραφή - διαφορετικά μπορεί οποιοσδήποτε να στείλει ό,τι θέλει στο URL σας.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyVolaiSignature(
rawBody: string,
signatureHeader: string | null,
secret: string,
toleranceSecs = 300,
): boolean {
if (!signatureHeader) return false;
const parts = new Map<string, string>();
for (const piece of signatureHeader.split(",")) {
const idx = piece.indexOf("=");
if (idx > 0) parts.set(piece.slice(0, idx), piece.slice(idx + 1));
}
const t = parts.get("t");
const v1 = parts.get("v1");
if (!t || !v1) return false;
const age = Math.abs(Date.now() / 1000 - Number(t));
if (!Number.isFinite(age) || age > toleranceSecs) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(v1, "utf8");
return a.length === b.length && timingSafeEqual(a, b);
}
// Next.js route handler - το rawBody ΠΡΕΠΕΙ να είναι ακριβώς αυτό που ήρθε από το δίκτυο (request.text()), όχι JSON.parse και πάλι σε string.
export async function POST(request: Request) {
const rawBody = await request.text();
const valid = verifyVolaiSignature(
rawBody,
request.headers.get("volai-signature"),
process.env.VOLAI_WEBHOOK_SECRET!,
);
if (!valid) return new Response("invalid signature", { status: 401 });
const event = JSON.parse(rawBody);
// ... επεξεργασία στο παρασκήνιο, δείτε Συμβουλές παρακάτω
void event;
return new Response("ok", { status: 200 });
}import hashlib
import hmac
import time
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_..."
def verify_volai_signature(raw_body, signature_header, secret, tolerance_secs=300):
if not signature_header:
return False
parts = {}
for piece in signature_header.split(","):
if "=" in piece:
key, value = piece.split("=", 1)
parts[key] = value
t = parts.get("t")
v1 = parts.get("v1")
if not t or not v1:
return False
if abs(time.time() - float(t)) > tolerance_secs:
return False
payload = f"{t}.{raw_body}".encode("utf-8")
expected = hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
@app.route("/webhooks/volai", methods=["POST"])
def volai_webhook():
raw_body = request.get_data(as_text=True)
signature = request.headers.get("Volai-Signature", "")
if not verify_volai_signature(raw_body, signature, WEBHOOK_SECRET):
return "invalid signature", 401
event = request.get_json()
# ... επεξεργασία στο παρασκήνιο, δείτε Συμβουλές παρακάτω
del event
return "ok", 200Προσαρμοσμένες κεφαλίδες
Αν ο προορισμός δέχεται μόνο αιτήματα με έλεγχο ταυτότητας (π.χ. αυτοματισμός στο Cursor ή δικό σας API πίσω από πύλη), προσθέστε το κλειδί ως προσαρμοσμένη κεφαλίδα. Αποστέλλεται με κάθε παράδοση και με το δοκιμαστικό συμβάν, π.χ. Authorization: Bearer <κλειδί>. Τη ρυθμίζετε με το πεδίο headers στο PUT /v1/webhook, στο εργαλείο MCP set_webhook ή στην πύλη στις Συνδέσεις και API στην ενότητα Webhooks.
curl -X PUT https://volai.cz/v1/webhook \
-H "Authorization: Bearer $VOLAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://api.example.com/hooks/volai", "events": ["message.received", "call.completed", "call.missed"], "headers": {"Authorization": "Bearer YOUR_TARGET_KEY"}}'- Το πολύ 5 κεφαλίδες, κάθε τιμή 1 έως 1000 χαρακτήρες, μόνο εκτυπώσιμα ASCII (χωρίς τόνους). Τα ονόματα δεν κάνουν διάκριση πεζών-κεφαλαίων και δεν επιτρέπεται να επαναλαμβάνονται. Τις
host,content-type,content-length,volai-signatureκαι τις hop-by-hop κεφαλίδες τις ορίζει η ίδια η παράδοση, αυτές δεν μπορούν να οριστούν. - Οι τιμές είναι μυστικά: το
GET /v1/webhookτις επιστρέφει μόνο συντομευμένες (Bear***). Αν στείλετε μια τέτοια μάσκα πίσω, παραμένει η αποθηκευμένη τιμή. - Αν παραλείψετε το
headers, οι αποθηκευμένες κεφαλίδες διατηρούνται, το{}τις διαγράφει. Όταν αλλάξετε το URL σε άλλη διεύθυνση (άλλος τομέας, σχήμα ή θύρα, ακόμη και με επιπλέονwww.), οι αποθηκευμένες κεφαλίδες απορρίπτονται και εισάγετε ξανά τις τιμές ολόκληρες: το κλειδί δεν το στέλνουμε σε ξένη διεύθυνση. Αν εκείνη τη στιγμή στείλετε μάσκα αντί για τιμή, επιστρέφεται το σφάλμαinvalid_webhook_headers(400), όπως και για απαγορευμένη ή μη έγκυρη κεφαλίδα. - Σε ανακατεύθυνση προς άλλη διεύθυνση οι προσαρμοσμένες κεφαλίδες δεν στέλνονται, η υπογραφή
Volai-Signatureστέλνεται. Και η ανακατεύθυνση από τοexample.comστοwww.example.comείναι άλλη διεύθυνση, γι' αυτό δηλώστε στο webhook απευθείας την τελική.
Η υπογραφή Volai-Signature συνεχίζει να έρχεται δίπλα στις προσαρμοσμένες κεφαλίδες. Η κεφαλίδα λέει ότι το αίτημα επιτρέπεται να περάσει, η υπογραφή ότι το έστειλε πράγματι το volai.
Επαναλήψεις σε περίπτωση σφάλματος
Αν το endpoint σας δεν απαντήσει με επιτυχή κατάσταση (2xx), το volai θα δοκιμάσει άλλες δύο φορές - συνολικά δηλαδή 3 προσπάθειες με παύση 3 δευτερολέπτων ανάμεσά τους. Στα συμβάντα κλήσεων (`call.completed`, `call.failed`, `call.no_answer`, `call.missed`) και στο `message.received` η παράδοση δεν τελειώνει εκεί: αν αποτύχουν και οι τρεις προσπάθειες, το συμβάν μένει στην ουρά και η παράδοση επαναλαμβάνεται ακόμη αρκετές φορές τα επόμενα λεπτά, το πολύ έως 5 γύρους. Γι' αυτό το σώμα φέρει `event_id` - με αυτό φιλτράρετε την παράδοση ως διπλή, αν σας φτάσει κατ' εξαίρεση περισσότερες φορές· η επαναλαμβανόμενη παράδοση φέρει ακριβώς το ίδιο σώμα με την πρώτη (απλώς σταλμένο λίγα λεπτά αργότερα). Το `message.sent` δεν έχει ουρά: μετά από 3 αποτυχημένες προσπάθειες καταγράφεται και δεν ξαναστέλνεται.
Εξαίρεση είναι το δοκιμαστικό συμβάν παρακάτω - αυτό στέλνεται μόνο μία φορά, χωρίς επαναλήψεις, ώστε να λάβετε απάντηση αμέσως.
Το webhook δεν είναι η μοναδική πηγή αλήθειας
Το webhook φέρει μόνο μια σύνοψη - για ό,τι πραγματικά έχει σημασία (τιμολόγηση, έλεγχος, κείμενο μηνύματος) προτιμήστε να τραβάτε και την πλήρη εγγραφή από το REST API: GET /v1/calls/{id} και GET /v1/messages/{id}, οποτεδήποτε τη ζητήσετε.
Δοκιμαστικό συμβάν
Πριν περιμένετε την πρώτη πραγματική κλήση, ελέγξτε την παράδοση αμέσως - το POST /v1/webhook/test (δείτε REST API) στέλνει στο URL σας ένα δείγμα συμβάντος webhook.test, ΑΝΕΞΑΡΤΗΤΑ από το ποια events έχετε επιλέξει στο PUT /v1/webhook - ελέγχεται η δυνατότητα παράδοσης του ίδιου του URL και της υπογραφής, όχι η συνδρομή σε συγκεκριμένα συμβάντα. Το πολύ 10 προσπάθειες την ώρα. Το τεστ στέλνει μόνο το webhook.test, όχι τα υπόλοιπα συμβάντα: τη μορφή του message.received θα τη δοκιμάσετε μόνο με πραγματικό SMS στο αριθμός κινητού ή στον αριθμός για κλήσεις και SMS σας (παράδειγμα παραπάνω).
Επισκόπηση παραδόσεων
Τις τελευταίες 50 παραδόσεις (επιτυχείς, αποτυχημένες και δοκιμαστικές) θα δείτε μέσω GET /v1/webhook/deliveries (δείτε REST API), ή απευθείας στην πύλη κάτω από τη φόρμα του webhook. Την ίδια σύνοψη φέρει και το GET /v1/webhook στο πεδίο recentDeliveries.
Συμβουλές
- Απαντήστε 200 όσο πιο γρήγορα γίνεται - τον έλεγχο της υπογραφής και την αποθήκευση του ID τα προλαβαίνετε αμέσως, τη βαριά δουλειά (email, υπολογισμοί, κλήσεις άλλων API) μεταθέστε την στο παρασκήνιο.
- Το endpoint πρέπει να είναι δημόσια προσβάσιμο μέσω HTTPS - το
localhostκαι το self-signed πιστοποιητικό δεν λειτουργούν. - Χρησιμοποιήστε το
data.id(ID της κλήσης ή του μηνύματος) για να φιλτράρετε διπλότυπα, αν τυχόν σας φτάσει το ίδιο συμβάν περισσότερες φορές. - Δοκιμάστε τοπικά μέσω σήραγγας (π.χ. ngrok) και αλλάξτε προσωρινά το URL στο
PUT /v1/webhookστη διεύθυνσή της.
Σχετικά
MCP server
Connect Claude Code, Codex CLI, Codex desktop, Cursor and other AI editors.
REST API
Complete reference for every endpoint: numbers, calls, SMS, mobile numbers for SMS, agents and their drafts, tools, recordings, webhooks, do-not-call list, relay, account, tasks, credit, documents, calendars and integrations.
Voice agent
System prompt, tools, handoff to a human, recordings and structured call data.