Μετάβαση στο περιεχόμενο

Οδηγοί

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

json
{
  "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

json
{
  "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.

json
{
  "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 δεν προκαλεί συμβάν μέχρι να το συμπληρώσουν τα αρχεία του παρόχου.

json
{
  "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

json
{
  "event": "message.sent",
  "ts": 1756111640000,
  "data": {
    "id": "msg_7c1f9a2e",
    "to": "+420777123456",
    "status": "sent",
    "priceHal": 136,
    "smsNumber": null
  }
}

message.received

json
{
  "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 σας.

typescript
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 });
}
python
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.

bash
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 στη διεύθυνσή της.