Webhook ו-API להעלאת הקלטות: חיבור מרכזייה ל-VoiceBox
שתי נקודות קצה חתומות: אחת לקליטת מטא-דאטה של שיחה שהסתיימה, ואחת להעלאת קובץ ההקלטה עצמו. כאן מתועד בדיוק מה הן מקבלות ומה הן מחזירות, כדי שאפשר יהיה לבדוק התאמה לפני שמדברים עם מישהו.
כל בקשה נחתמת ב-HMAC-SHA256 ונבדקת מול חלון זמן. אין מצב שבו קליטה עובדת בלי חתימה.
- חתימה
- HMAC-SHA256
- חלון זמן לבקשה
- 5 דקות
- גוף Webhook
- עד 256KiB
- בקשת העלאה
- עד 200MiB
למי המדריך הזה
למי שצריך להזרים שיחות מהמרכזייה, מה-CRM או משרת ההקלטות אל VoiceBox. אין SDK ואין ספרייה להתקין: שתי נקודות קצה ב-HTTPS, חתימה אחת ותשובת JSON. כל מערכת שיודעת לבצע בקשת POST חתומה יכולה להתחבר. מערכת שאינה יודעת לקרוא ל-HTTP או לחתום בעצמה תצטרך תוכנה שתעשה זאת עבורה לצד המרכזייה, ואת זה הדף הזה אינו מכסה.
מה שכתוב כאן הוא ההתנהגות בפועל של נקודות הקצה - שדות, קודים וגבולות - ולא תוכנית עתידית. מה שלא כתוב כאן, אל תניחו שקיים.
שתי נקודות הקצה
בשתיהן {connectorId} הוא מזהה מקור הקליטה שלכם, ושתיהן מקבלות POST בלבד:
| נתיב | סוג תוכן | מה שולחים | מה נחתם |
|---|---|---|---|
POST /api/ingest/{connectorId} | application/json | מטא-דאטה של שיחה שהסתיימה | גוף הבקשה, בדיוק כפי שנשלח |
POST /api/ingest/{connectorId}/upload | multipart/form-data | מטא-דאטה יחד עם קובץ האודיו | שדה meta בלבד |
מקורות קליטה מוקצים לחשבון על ידינו. אחרי שהוקצה מקור, מזהה מקור הקליטה ומפתח החתימה מוצגים בקונסולה במסך מקורות קליטה - שם גם מחליפים מפתח, אך אין שם יצירה של מקור חדש או הפעלה של מקור מושבת. המפתח נגזר בעת ההצגה ואינו נשמר כעמודה בבסיס הנתונים, וכל חשיפה שלו נרשמת ביומן הביקורת. החלפה מבטלת מיד את המפתח הקודם, וכל מערכת שממשיכה לשלוח עם הישן נעצרת - ולכן מחליפים בחלון תחזוקה ולא באמצע יום עבודה.
אל תשמרו את המפתח בקוד המקור ואל תשלחו אותו בדוא״ל. כל הדוגמאות כאן קוראות אותו ממשתנה סביבה, בכוונה.
חתימה על כל בקשה
לכל בקשה מצרפים שתי כותרות:
x-voicebox-timestamp- הזמן הנוכחי בשניות מאז 1970, כמספר שלם.x-voicebox-signature- HMAC-SHA256 בהקסדצימלי על המחרוזת<timestamp>.<signed-string>, עם מפתח החתימה של מקור הקליטה.
וכאן הפרט שמפיל את רוב האינטגרציות בפעם הראשונה: ב-Webhook נחתם גוף הבקשה כפי שנשלח, ובהעלאת קובץ נחתם שדה meta בלבד. לכן ב-Webhook יש לחתום על אותה מחרוזת JSON שיוצאת על הקו - לא לחתום על אובייקט ולסדר אותו מחדש לפני השליחה, כי הבייטים ישתנו והחתימה תיפסל.
בהעלאה חשוב להכיר את הגבול הזה במדויק: החתימה מאמתת את המטא-דאטה בלבד ואינה מאמתת את בייטים של האודיו. הקובץ מוגן בהעברה על ידי TLS כמו שאר הבקשה, אך אין לו חתימה משלו. אנחנו מחשבים SHA-256 על הבייטים שהגיעו ומחזירים אותו בתשובה - זו טביעת אצבע של מה שהתקבל, לא הוכחה שהוא זהה לקובץ שיצא מכם. מי שנדרש להוכחה כזו צריך לחשב SHA-256 מקומי לפני השליחה ולהשוות אותו לערך שבתשובה; דוגמת ה-Node להעלאה עושה בדיוק את זה.
החתימה מושווית בזמן קבוע, ובקשה שחותמת הזמן שלה רחוקה מהשעון שלנו ביותר מחמש דקות לכל כיוון נדחית. זה מצמצם את חלון השידור החוזר של בקשה שנלכדה ברשת, אך אינו מונע שידור חוזר בתוך אותן חמש דקות; מה שמונע שיחה כפולה הוא מזהה חיצוני יציב, שגורם לשידור חוזר להתמזג עם השיחה הקיימת במקום ליצור אחת נוספת. המשמעות המעשית הנוספת: השעון בשרת השולח חייב להיות מסונכרן. שעון שסוטה בעשר דקות נראה בדיוק כמו מפתח שגוי, ושתי התקלות מחזירות 401.
Webhook של מטא-דאטה
נשלח בסיום שיחה ומכיל את מה שהמרכזייה יודעת עליה. האודיו אינו נמשך בתוך הבקשה הזו: התשובה חוזרת מהר כדי שהמרכזייה לא תיכנס למעגל ניסיונות חוזרים, וההקלטה נמשכת אחר כך מהכתובת שנשלחה. גוף הבקשה מוגבל ל-256KiB, והגודל נספר על הזרם עצמו ולא לפי מה שהשולח הצהיר.
שדות והשמות המקובלים עליהם
אין צורך לשנות שמות שדות במרכזייה אם הם כבר אחד מהשמות המוכרים. לכל שדה נבדקת רשימת מועמדים לפי הסדר, והראשון שאינו ריק הוא שנקלט:
| מה זה | חובה | שמות מקובלים |
|---|---|---|
| מזהה השיחה במרכזייה | כן | call_id · callId · id · uniqueid |
| זמן תחילת השיחה | כן | started_at · startedAt · start_time · startTime · timestamp · date |
| מספר הצרכן | כן | customer_number · to · destination · dst · called_number · phone |
| כיוון השיחה | לא | direction · call_direction · type |
| משך בשניות | לא | duration_sec · duration · billsec · talk_time |
| זמן סיום | לא | ended_at · endedAt · end_time · endTime |
| מספר או שלוחה של הנציג | לא | agent_number · from · source · src · extension · caller |
| שם הנציג | לא | agent_name · agentName · user_name |
| כתובת ההקלטה | לא | recording_url · recordingUrl · record_url · file_url · url |
| סטטוס מהמרכזייה | לא | disposition · status · result |
אם השמות אצלכם שונים לחלוטין, מפו אותם בצד שלכם לאחד השמות שברשימה. מה שנשלח נשמר במלואו לצד השיחה, גם שדות שלא מופו, כך ששום מידע לא הולך לאיבוד.
שלושה פרטים שמונעים תקלות
- זמנים. חותמת זמן בלי אזור זמן מפורשת מפוענחת כשעון ישראל (
Asia/Jerusalem). מספר בן עשר ספרות נקרא כשניות, ובן שלוש-עשרה כמילישניות. הבטוח ביותר הוא ISO 8601 עם היסט, למשל2027-04-01T09:14:22+03:00. - משך. אפשר לשלוח מספר שניות או מחרוזת בפורמט
00:03:34. כשאין משך אבל יש זמן סיום, המשך מחושב מההפרש. - מספר טלפון. בכל צורה שהמרכזייה מייצרת:
050-000-1234,+972500001234או972500001234. אנחנו שומרים את כל הצורות כדי שהצרכן ימצא את השיחה שלו לפי מה שהוא מקליד.
גוף בקשה לדוגמה
{
"call_id": "pbx-2027-04-01-000871",
"direction": "outbound",
"start_time": "2027-04-01T09:14:22+03:00",
"duration": 214,
"customer_number": "050-000-1234",
"agent_number": "101",
"recording_url": "https://pbx.example/rec/000871.wav"
}כאשר נשלחת recording_url, האודיו נמשך ממנה אחרי שהשיחה נקלטה. הכתובת חייבת להיות ב-HTTP או HTTPS ונגישה מהאינטרנט; כתובת שמפנה לרשת פנימית נדחית בכוונה, וזה בדיוק המקרה שבו יש להשתמש בנקודת הקצה להעלאת קובץ.
דוגמה: curl
# All values here are synthetic. Replace them with your own.
# Read the signing key into the environment first, without leaving it on disk:
# read -rs VOICEBOX_SIGNING_SECRET && export VOICEBOX_SIGNING_SECRET
: "${VOICEBOX_SIGNING_SECRET:?set the signing key before running this}"
CONNECTOR_ID='con_xxxxxxxxxxxxxxxxxxxxxx'
BODY='{"call_id":"pbx-2027-04-01-000871","direction":"outbound","start_time":"2027-04-01T09:14:22+03:00","duration":214,"customer_number":"050-000-1234","agent_number":"101","recording_url":"https://pbx.example/rec/000871.wav"}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$VOICEBOX_SIGNING_SECRET" | sed 's/^.*= //')
curl -sS -X POST "https://voicebox.co.il/api/ingest/$CONNECTOR_ID" \
-H 'content-type: application/json' \
-H "x-voicebox-timestamp: $TS" \
-H "x-voicebox-signature: $SIG" \
--data-raw "$BODY"דוגמה: Node 20 ומעלה
import { createHmac } from 'node:crypto';
const connectorId = process.env.VOICEBOX_CONNECTOR_ID;
const secret = process.env.VOICEBOX_SIGNING_SECRET;
if (!connectorId || !secret) {
throw new Error('Set VOICEBOX_CONNECTOR_ID and VOICEBOX_SIGNING_SECRET before running this.');
}
const endpoint = `https://voicebox.co.il/api/ingest/${connectorId}`;
// Serialise once, then sign and send that very same string. Signing an object
// and re-serialising it before sending produces different bytes, and the
// signature is checked against the bytes that arrive.
const body = JSON.stringify({
call_id: 'pbx-2027-04-01-000871',
direction: 'outbound',
start_time: '2027-04-01T09:14:22+03:00',
duration: 214,
customer_number: '050-000-1234',
agent_number: '101',
recording_url: 'https://pbx.example/rec/000871.wav',
});
const timestamp = Math.floor(Date.now() / 1000);
const signature = createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-voicebox-timestamp': String(timestamp),
'x-voicebox-signature': signature,
},
body,
});
const result = await response.json();
console.log(response.status, result);
if (!response.ok) {
throw new Error(`ingest failed: ${response.status} ${result.code ?? ''}`.trim());
}תשובת הצלחה
HTTP/1.1 201 Created
{
"ok": true,
"callId": "cal_7q4m2xk9vd3hbn5r8ts0jw",
"created": true,
"scope": "inScope",
"recordingRequired": true,
"audioPending": true
}created- האם נוצרה שיחה חדשה. חדשה מוחזרת עם201, שיחה שכבר נקלטה עם200.scope- סיווג ראשוני של השיחה מול חובת ההקלטה, אחד מ-inScope · outOfScope · indeterminate.recordingRequired- האם לשיחה הזו נדרשת הקלטה שמורה.audioPending- האם נשלחה כתובת הקלטה שעדיין ממתינה למשיכה.
העלאת קובץ אודיו
כשאין כתובת שאפשר למשוך ממנה - מרכזייה ברשת פנימית, שרת קבצים, סוכן שרץ לצד המרכזייה - שולחים את המטא-דאטה ואת הקובץ יחד ב-multipart/form-data עם שני חלקים: meta, מחרוזת JSON שהיא גם המחרוזת הנחתמת, ו-audio, הקובץ עצמו.
שדה meta נקרא בשמות מדויקים ואינו משתמש בשמות המקובלים של ה-Webhook. חובה: externalId, counterparty ו-startedAt תקין. רשות: agentRef, direction (אחד מ-outbound · inbound · unknown, ברירת המחדל unknown) ו-durationSeconds. שדות נוספים פשוט לא ייקראו.
הבקשה חייבת לכלול Content-Length: בקשה שאינה מצהירה על אורך נדחית בקוד 411. הגבול הוא 200MiB לכלל הבקשה ולא לקובץ בלבד - הוא נבדק על האורך המוצהר של גוף ה-multipart כולו, כולל שדה meta וגבולות החלקים. לכן קובץ האודיו צריך להיות מעט מתחת ל-200MiB, ולא בדיוק עליו.
פורמטים, ומה נחשב הקלטה תקינה
הפורמט מזוהה מהבייטים עצמם ולא מהסוג שהוצהר, כי המערכת שטעתה בהקלטה היא בדיוק זו שמצהירה על הסוג. מזוהים WAV, MP3, Ogg (Opus או Vorbis), MP4/M4A, FLAC, AMR-NB ו-WebM.
שתי בדיקות נוספות רצות על כל קובץ, ושתיהן מדווחות בתשובה במקום להיבלע בשקט:
- קובץ ריק נדחה מיד בקוד
422. - קובץ שאינו אודיו מזוהה, או WAV מסוג PCM שהוא שקט לחלוטין, נשמרים אך מסומנים בבידוד, והתשובה חוזרת עם
quarantined: trueועם הסבר בעברית בשדהreason. שניהם הסימפטום הקלאסי של ערוץ הקלטה שאינו מחובר כראוי, ועדיף לגלות אותם עכשיו ולא ביום שבו לקוח מבקש את השיחה.
שימו לב לגבול של בדיקת השקט: היא מבוססת על מדידת עוצמה בדגימות PCM, ולכן חלה על WAV לא דחוס בלבד. הקלטה שקטה שהגיעה כ-MP3, Ogg או פורמט דחוס אחר תיקלט כרגיל בשלב הקליטה ולא תסומן בבידוד בשל שקט. פורמט שאינו מזוהה מסומן בבידוד בכל מקרה.
דוגמה: curl
: "${VOICEBOX_SIGNING_SECRET:?set the signing key before running this}"
CONNECTOR_ID='con_xxxxxxxxxxxxxxxxxxxxxx'
META='{"externalId":"pbx-2027-04-01-000871","startedAt":"2027-04-01T09:14:22+03:00","counterparty":"050-000-1234","agentRef":"101","direction":"outbound","durationSeconds":214}'
TS=$(date +%s)
# The signature covers the meta field only, never the multipart body.
SIG=$(printf '%s' "$TS.$META" | openssl dgst -sha256 -hmac "$VOICEBOX_SIGNING_SECRET" | sed 's/^.*= //')
curl -sS -X POST "https://voicebox.co.il/api/ingest/$CONNECTOR_ID/upload" \
-H "x-voicebox-timestamp: $TS" \
-H "x-voicebox-signature: $SIG" \
-F "meta=$META" \
-F "audio=@call-000871.wav;type=audio/wav"דוגמה: Node 20 ומעלה
import { createHash, createHmac } from 'node:crypto';
import { readFile } from 'node:fs/promises';
const connectorId = process.env.VOICEBOX_CONNECTOR_ID;
const secret = process.env.VOICEBOX_SIGNING_SECRET;
if (!connectorId || !secret) {
throw new Error('Set VOICEBOX_CONNECTOR_ID and VOICEBOX_SIGNING_SECRET before running this.');
}
const endpoint = `https://voicebox.co.il/api/ingest/${connectorId}/upload`;
const audio = await readFile('./call-000871.wav');
const meta = JSON.stringify({
externalId: 'pbx-2027-04-01-000871',
startedAt: '2027-04-01T09:14:22+03:00',
counterparty: '050-000-1234',
agentRef: '101',
direction: 'outbound',
durationSeconds: 214,
});
const form = new FormData();
form.append('meta', meta);
form.append('audio', new Blob([audio], { type: 'audio/wav' }), 'call-000871.wav');
const timestamp = Math.floor(Date.now() / 1000);
// Only meta is signed. The signature does not authenticate the audio bytes;
// those are protected in transit by TLS like the rest of the request.
const signature = createHmac('sha256', secret).update(`${timestamp}.${meta}`).digest('hex');
const response = await fetch(endpoint, {
method: 'POST',
// No content-type header: fetch sets the multipart boundary and the
// Content-Length, and this endpoint refuses a body that declares no length.
headers: {
'x-voicebox-timestamp': String(timestamp),
'x-voicebox-signature': signature,
},
body: form,
});
const result = await response.json();
console.log(response.status, result);
if (!response.ok) {
throw new Error(`upload failed: ${response.status} ${result.code ?? ''}`.trim());
}
// The response hash describes the bytes that arrived. Comparing it with a
// digest computed here is what makes it evidence about the file on this disk,
// so a response without one is a failed check rather than a skipped one.
if (typeof result.sha256 !== 'string') {
throw new Error('no sha256 in the response: the upload could not be verified.');
}
const local = createHash('sha256').update(audio).digest('hex');
if (result.sha256 !== local) {
throw new Error(`stored ${result.sha256} does not match local ${local}`);
}תשובת הצלחה
HTTP/1.1 201 Created
{
"ok": true,
"callId": "cal_7q4m2xk9vd3hbn5r8ts0jw",
"recordingId": "rec_3n8k5wq2ph7dtx4bs9mvj0",
"sha256": "9f2c1d7a4b03e8115c6d29ab74f0e3d582c9147bb60ae3f21d84c5907ea6b31c",
"quarantined": false,
"reason": null
}ההעלאה מחזירה 201 גם כשהשיחה כבר היתה קיימת, ואינה מחזירה את השדות created ו-scope שמופיעים בתשובת ה-Webhook.
sha256 הוא הגיבוב שחישבנו על הבייטים שהתקבלו אצלנו. הוא מזהה את מה שנשמר, אך לבדו אינו מעיד שזה הקובץ שיצא מכם - לשם כך חשבו SHA-256 על הקובץ המקומי לפני השליחה והשוו בין הערכים. התאמה בין השניים היא ראיה חזקה לכך שהבייטים שהתקבלו הם הבייטים שקראתם מהדיסק, ואי התאמה היא סימן ודאי שמשהו בדרך שינה או קטע את הקובץ.
מניעת כפילויות
שיחה מזוהה לפי צירוף של מקור הקליטה והמזהה החיצוני שנשלח - call_id ב-Webhook, externalId בהעלאה. שידור חוזר של אותו מזהה אינו יוצר שיחה שנייה, אבל התשובה נראית אחרת בכל אחת מנקודות הקצה:
- Webhook: שיחה חדשה מוחזרת עם
201ו-created: true, ושידור חוזר מוחזר עם200ו-created: falseואותוcallId. - העלאת קובץ: התשובה היא
201בכל מקרה ואין בהcreated. השיחה הקיימת נעשית בה שימוש חוזר, וההקלטה שלה מתעדכנת בקובץ החדש. כלומר אין דרך להסיק מקוד התשובה אם זו הפעם הראשונה - מי שצריך לדעת, ישווה את ה-callIdשחזר למה שכבר רשום אצלו.
לכן המזהה חייב להיות יציב, כלומר אותו ערך בכל ניסיון חוזר של אותה שיחה, וייחודי בין שיחות שונות. מזהה שנוצר מחדש בכל ניסיון מייצר שיחות כפולות, ומזהה שחוזר על עצמו בין שיחות שונות גורם לשנייה להיבלע בראשונה. העלאה חוזרת של אותו מזהה עם קובץ אחר מחליפה את האודיו השמור ואינה מוסיפה שיחה.
קודי שגיאה
כל שגיאה חוזרת כ-JSON בצורה { "ok": false, "code": "...", "error": "..." }. השדה code יציב ומיועד לקוד שלכם, והשדה error הוא הסבר בעברית שנועד לאדם שיקרא את היומן.
HTTP/1.1 401 Unauthorized
{
"ok": false,
"code": "connector.badSignature",
"error": "..."
}| HTTP | code | מתי | מה עושים |
|---|---|---|---|
400 | connector.badJson · badJson · badRequest | הגוף אינו JSON תקין, שדה meta אינו JSON, הבקשה אינה multipart תקין, חסר meta או audio, או Content-Length שאינו מספר תקין | לתקן את הבקשה; ניסיון חוזר יחזיר את אותה שגיאה |
401 | connector.badSignature | חתימה חסרה או שגויה, חתימה במפתח של מקור אחר, או חותמת זמן מחוץ לחלון חמש הדקות | לבדוק את המפתח, לוודא שנחתמה המחרוזת הנכונה, ולסנכרן שעון |
403 | connector.disabled | מקור הקליטה מושבת | מקור מושבת אינו נפתח מהקונסולה - יש לפנות אלינו |
404 | connector.notFound | מזהה מקור הקליטה שבנתיב אינו מוכר | להעתיק מחדש את המזהה מהקונסולה |
409 | ingest.conflict | התנגשות נדירה בין שתי בקשות שהגיעו במקביל על אותה שיחה | ניסיון חוזר אחד אחרי השהיה קצרה |
411 | lengthRequired | העלאת קובץ בלי כותרת Content-Length | לשלוח את אורך הגוף; רוב הלקוחות עושים זאת לבד |
413 | tooLarge | גוף Webhook מעל 256KiB, או בקשת העלאה שכולה מעל 200MiB | לצמצם את המטא-דאטה, לשלוח קידוד קטן יותר (למשל MP3 במקום WAV לא דחוס), או לשלוח recording_url במקום הקובץ |
422 | connector.unmappable · badMeta · recording.empty | חסר מזהה שיחה, זמן התחלה תקין או מספר צרכן, או שקובץ האודיו ריק | להשלים את מה שחסר לפני שליחה חוזרת |
500 | internal | תקלה זמנית אצלנו | לנסות שוב עם השהיה מדורגת |
ניסיונות חוזרים
- לנסות שוב: תקלות רשת ו-
5xx, עם השהיה מדורגת. הקליטה אידמפוטנטית לפי המזהה החיצוני, ולכן ניסיון חוזר בטוח ולא ייצור שיחה כפולה. - ניסיון חוזר אחד, אחרי השהיה קצרה:
409בלבד. זו התנגשות רגעית בין שתי בקשות על אותה שיחה, והיא נפתרת מעצמה. - לא לנסות שוב עם אותה בקשה:
400,401,403,404,411,413ו-422. כולן מתארות בקשה שצריכה להשתנות - כולל411, שהוא צורת הבקשה ולא תקלה זמנית - וניסיון חוזר זהה רק יטביע את התקלה האמיתית מתחת ליומן מלא בשגיאות זהות. - לחתום מחדש בכל ניסיון. חותמת זמן ישנה נפסלת אחרי חמש דקות, ולכן אין לשמור חתימה ולשלוח אותה שוב מאוחר יותר.
- לא לוותר על השיחה. אם כל הניסיונות נכשלו, עדיף להחזיק את הרשומה בתור מקומי ולשדר אותה שוב בהמשך: מזהה יציב מבטיח שהיא תיקלט פעם אחת בלבד, גם כעבור יומיים.
בשתי נקודות הקצה שמתועדות כאן, מונה הכשלים מתקדם על סוג מצומצם של תקלות בלבד: כשל באימות החתימה, וכשל בפענוח המטא-דאטה של ה-Webhook - כלומר connector.unmappable. אחרי חמישה כשלים רצופים כאלה מקור הקליטה מסומן כתקול. תקלות אחרות בנקודות הקצה - JSON לא תקין, חלקי multipart חסרים, meta לא תקין, קובץ ריק או שגיאת שרת - מוחזרות אליכם אך אינן מקדמות את המונה במסלול הזה. ל-recording_url יש מסלול נפרד: כשל בהורדת ההקלטה מסמן את המקור כמוגבל כבר בכשל הראשון, ואחרי שלושה כשלים רצופים הוא מסומן כתקול. הצלחה מאפסת את הרצף.
רשימת בדיקה לפני חיבור
- שעון השרת השולח מסונכרן ב-NTP. סטייה של יותר מחמש דקות נראית בדיוק כמו מפתח שגוי.
- מפתח החתימה נשמר במנהל סודות או במשתנה סביבה, לא בקוד המקור ולא בקובץ שנשמר בגיט.
- לכל שיחה יש מזהה יציב וייחודי, שנשאר זהה בכל ניסיון חוזר.
- זמן תחילת השיחה נשלח עם אזור זמן מפורש.
- מספר הצרכן נשלח תמיד, גם כשהוא מגיע חלקי מהמרכזייה.
- הרצתם את ערכת הבדיקה במצב יבש והשוויתם את המחרוזת החתומה למה שהמערכת שלכם שולחת.
- הבדיקה הראשונה מתבצעת מול מקור קליטה ייעודי לבדיקות שהוקצה לחשבון, ולא מול מקור הייצור.
- יש טיפול נפרד בשגיאות
4xxובכשלים זמניים, ויומן שרושם אתcodeשחזר. - אם אתם שולחים
recording_url, הכתובת נגישה מהאינטרנט ולא רק מהרשת הפנימית שלכם.
ערכת בדיקה
קובץ אחד ל-Node 20 ומעלה, בלי תלויות. הוא בונה בקשה סינתטית לשתי נקודות הקצה, מדפיס את הכותרות, את המחרוזת המדויקת שנחתמה ואת פקודת ה-curl המקבילה, ואינו שולח דבר אלא אם הוספתם --send. מפתח החתימה נקרא ממשתנה סביבה או מקובץ בלבד ולא מארגומנט בשורת הפקודה, כדי שלא ידלוף להיסטוריית הפקודות או לרשימת התהליכים במחשב משותף.
# Dry run: prints the signed headers, the exact signed string and an
# equivalent curl command. Sends nothing.
node voicebox-webhook.mjs --connector con_xxxxxxxxxxxxxxxxxxxxxx
# The same, for the upload endpoint.
node voicebox-webhook.mjs --mode upload --connector con_xxxxxxxxxxxxxxxxxxxxxx \
--audio ./call-000871.wav
# Actually send, against a test connector. The key is read from the
# environment, never from an argument.
read -rs VOICEBOX_SIGNING_SECRET && export VOICEBOX_SIGNING_SECRET
node voicebox-webhook.mjs --connector con_xxxxxxxxxxxxxxxxxxxxxx --sendבמצב העלאה הכלי מדפיס גם SHA-256 מקומי של הקובץ, וכששולחים בפועל הוא משווה אותו לערך שחזר בתשובה ומדווח על אי-התאמה. כשאין מפתח בסביבה הוא חותם במפתח אקראי חד-פעמי, כדי שאפשר יהיה לראות את מבנה הבקשה, ומסרב לשלוח. שליחה בפועל מתבצעת ב-HTTPS בלבד, למעט מול localhost, ואין דגל שמדלג על חתימה - נקודת קצה שמקבלת רשומות שיחה בלי אימות היא נקודת קצה שכל אחד יכול להזין אליה שיחה שלא היתה.
גרסה ושינויים
לנתיבים אין מספר גרסה, והמסמך הזה מתאר את ההתנהגות הנוכחית. שינוי שאינו תואם לאחור יופיע כאן, עם תאריך.
- גרסה 1.0 · 28 ביולי 2026
- פרסום ראשון: שתי נקודות הקצה, החתימה וגבולות מה שהיא מאמתת, חלון הזמן, שדות ושמות מקובלים, פורמטי אודיו ובידוד, מניעת כפילויות והבדלי התשובה בין נקודות הקצה, קודי שגיאה, ניסיונות חוזרים וערכת בדיקה.
מה קורה אחרי הקליטה
שיחה שנקלטה נכנסת לניהול ההקלטות: שמירה לפי מדיניות, חיפוש לפי לקוח, עובד או תאריך, ומסירה מאובטחת ללקוח שמבקש את השיחה שלו. כך נראה ניהול ההקלטות, וכאן ההסבר על החובה עצמה. המפרט הזה פתוח לקריאה בלי חשבון, והמחיר של השירות שהוא מתחבר אליו מוצג לפני ההרשמה ולא אחריה.
שאלות שחוזרות
- איזה מידע חייב להישלח על כל שיחה?
- מזהה שיחה שנשאר יציב בין ניסיונות, זמן תחילת שיחה שניתן לפענוח, ומספר הטלפון של הצרכן. בלי מזהה אי אפשר למנוע כפילויות, בלי זמן התחלה אי אפשר לחשב את תקופת השמירה, ובלי מספר הצרכן אי אפשר למסור לו את ההקלטה כשיבקש. כל השאר רשות.
- איך חותמים על בקשה?
- מחשבים HMAC-SHA256 בהקסדצימלי על המחרוזת timestamp.body עם מפתח החתימה של מקור הקליטה, ושולחים אותה בכותרת x-voicebox-signature לצד x-voicebox-timestamp שמכילה את הזמן בשניות. בקשה שהחתימה בה אינה תואמת, או שחותמת הזמן שלה רחוקה ביותר מחמש דקות, נדחית בקוד 401.
- מה בדיוק נחתם בהעלאת קובץ אודיו?
- שדה meta בלבד. החתימה מאמתת את המטא-דאטה, ולא את בייטים של האודיו: אלה מוגנים בהעברה על ידי TLS. השרת מחשב SHA-256 על הבייטים שהתקבלו ומחזיר אותו בתשובה, וכדי לוודא שהם זהים לקובץ המקורי אצלכם יש לחשב SHA-256 מקומי ולהשוות בין השניים.
- מה קורה כששולחים את אותה שיחה פעמיים?
- לא נוצרת שיחה כפולה. שיחה מזוהה לפי צירוף של מקור הקליטה והמזהה החיצוני. ב-Webhook שידור חוזר מחזיר את אותו callId עם created=false ובקוד 200 במקום 201; בהעלאת קובץ התשובה היא 201 בכל מקרה ואינה כוללת את השדות created ו-scope, והקובץ החדש מחליף את ההקלטה של אותה שיחה.
- אילו פורמטים של אודיו נקלטים?
- WAV, MP3, Ogg עם Opus או Vorbis, MP4 או M4A, FLAC, AMR-NB ו-WebM. מגבלת הגודל היא 200MiB לכלל בקשת ההעלאה, כולל המטא-דאטה וגבולות ה-multipart, ולכן על הקובץ להיות מעט מתחת לגבול הזה. קובץ בפורמט שאינו מזוהה נשמר אך מסומן בבידוד, וכך גם WAV מסוג PCM שהוא שקט לחלוטין; בדיקת השקט מתבצעת על PCM בלבד, ולכן פורמט דחוס אינו נבדק לשקט בשלב הקליטה. בכל מקרה של בידוד התשובה חוזרת עם quarantined=true והסבר בשדה reason.
- מתי כדאי לנסות לשלוח שוב?
- בשגיאות 5xx ובתקלות רשת, עם השהיה מדורגת וחתימה חדשה בכל ניסיון. בשגיאות 400, 401 ו-422 ניסיון חוזר יחזיר בדיוק את אותה תשובה, כי הבעיה היא בבקשה עצמה.
אפשר לבדוק את החיבור לפני שמעבירים אליו תנועה אמיתית
פתחו חשבון ובקשו מאיתנו מקור קליטה ייעודי לבדיקות. אחרי שהוקצה, הציגו את פרטי החיבור שלו בקונסולה והריצו מולו את ערכת הבדיקה. לשאלות טכניות על התיעוד הזה, או אם מקור קליטה מופיע כמושבת: hello@voicebox.co.il
המפרט ברור. מזהה המקור והמפתח שהבקשות נחתמות בו נוצרים בקונסולה, אחרי פתיחת חשבון.
פתיחת חשבון דורשת שם עסק, דוא״ל וסיסמה, וחיבור מקור ההקלטות מוגדר בשלב נפרד אחריה. כל המחשבונים, התבניות וקובצי ההורדה באתר זמינים בלי חשבון.
להמשך קריאה
- סוכן תיקייה למרכזייה ישנהאיך מעלים הקלטות מתיקייה במרכזייה ותיקה או ב-Asterisk: התקנה, תבניות שמות קבצים, אזור זמן, זיהוי כפילויות לפי תוכן, קובץ מצב, בידוד, חלון הסריקה וניטור.
- אינטגרציותארבעה מסלולי קליטה נתמכים ומאומתים, מה שאינו נתמך היום - כולל משיכה מ-S3 ומ-SFTP - ובודק התאמה שרץ בדפדפן ואומר איזה מסלול מתאים למרכזייה שלכם.
- מערכת הקלטת שיחות לעסקיםעמוד הקטגוריה של VoiceBox: מה מערכת לניהול הקלטות עושה מעל קובץ ההקלטה - קליטה מהמרכזייה, שמירה ומחיקה מבוקרת, איתור שיחות שלא נקלטו ומסירה ללקוח.