# Webhooks

במקום לשאול את ה-API שוב ושוב, רושמים כתובת HTTPS לאירועים. כשמשהו משתנה ב-banta - באפליקציה, דרך ה-API או בידי אוטומציה - banta שולחת לכתובת שלכם בקשת `POST` חתומה עם הפרטים.

## אירועים

| אירוע | נשלח כאשר |
|---|---|
| `lead.created` | נוצר ליד |
| `lead.status_changed` | הסטטוס של ליד השתנה (כולל מעבר בין שני סטטוסים מותאמים) |
| `lead.assigned` | ליד שויך למשתמש אחר, או שהשיוך הוסר |
| `lead.converted` | ליד הומר לתיק משכנתא |
| `lead.deleted` | ליד נמחק לצמיתות |
| `mortgage_file.created` | נוצר תיק משכנתא |
| `mortgage_file.status_changed` | הסטטוס של תיק משכנתא השתנה |
| `mortgage_file.assigned` | תיק משכנתא שויך למשתמש אחר, או שהשיוך הוסר |
| `mortgage_file.deleted` | תיק משכנתא נמחק (הועבר לסל המחזור או נמחק לצמיתות) |
| `task.created` | נוצרה משימה |
| `task.completed` | הסטטוס של משימה השתנה ל-`completed` |
| `payment.created` | נרשם תשלום בתיק משכנתא |
| `charge.paid` | הסטטוס של חיוב השתנה ל-`paid` |
| `milestone.completed` | הושלמה אבן דרך של תיק משכנתא |

`GET /webhooks/events` מחזיר את הקטלוג הזה. אירוע בדיקה, `ping`, נשלח רק דרך `POST /webhooks/{id}/test`.

## הרשמה

נקודות קצה של webhooks מנוהלות עם מפתח API שיש לו הרשאת webhooks **מפורשת** - `webhooks:read` ו-`webhooks:write` (ו-`webhooks:delete` לביטול הרשמה), `webhooks:*` או `admin`. ההרשאות הכלליות `read`/`write` לא כוללות אותה, כי מנוי שולח מידע אישי לכתובת חיצונית.

```bash
curl -X POST "https://app.banta.co.il/api-service/webhooks" \
  -H "x-api-key: $BANTA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/banta/webhooks",
    "events": ["lead.created", "lead.status_changed"],
    "description": "CRM sync"
  }'
```

התגובה כוללת את `secret` (`whsec_...`). **הוא מוצג פעם אחת בלבד** - שמרו אותו בסודות של השרת שלכם. אפשר להחליף אותו בהמשך עם `POST /webhooks/{id}/rotate-secret`; הסוד הקודם מפסיק לעבוד מיד.

- הכתובת חייבת להיות `https` בפורט ברירת המחדל, בלי שם משתמש או סיסמה, ואסור שתפנה ל-`localhost`, לשם מארח פנימי או לכתובת IP פרטית.
- לארגון יכולות להיות עד 20 נקודות קצה.
- שלחו בדיקה עם `POST /webhooks/{id}/test`, ועקבו אחריה ב-`GET /webhooks/{id}/deliveries`.

## הבקשה

```http
POST /banta/webhooks HTTP/1.1
Content-Type: application/json
User-Agent: banta-webhooks/1
X-Banta-Event: lead.status_changed
X-Banta-Event-Id: 0f1e2d3c-4b5a-4968-8776-655443322110
X-Banta-Delivery: 7c6b5a49-3827-4615-a493-827160594837
X-Banta-Timestamp: 1791975012
X-Banta-Signature: sha256=5d41402abc4b2a76b9719d911017c592...
```

```json
{
  "id": "0f1e2d3c-4b5a-4968-8776-655443322110",
  "type": "lead.status_changed",
  "created_at": "2026-10-14T09:30:12.418503+00:00",
  "data": {
    "object": "lead",
    "id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
    "changes": { "status": { "from": "new", "to": "contacted" } },
    "lead": {
      "id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
      "lead_number": "1042",
      "first_name": "דנה",
      "last_name": "לוי",
      "phone": "0501234567",
      "email": "dana@example.com",
      "status": "contacted",
      "status_custom": null,
      "source": "website",
      "assigned_to": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a99",
      "converted_to_mortgage_file_id": null,
      "created_at": "2026-10-13T16:05:44.120931+00:00"
    }
  }
}
```

- `data.object` הוא סוג האובייקט, וגם המפתח של התצלום שלו בתוך `data` (`lead`, `mortgage_file`, `task`, `payment`, `charge` או `milestone`).
- התצלום הוא האובייקט ברגע השינוי, מוגבל לרשימת שדות קבועה - לעולם לא מספרי זהות, הכנסות או מסמכים. את הרשומה המלאה שולפים עם `GET` ו-`data.id`.
- `changes` מפרט את שדות התצלום שהשתנו, עם `from` ו-`to`. באירועי יצירה ומחיקה הוא ריק.
- שינוי אחד יכול לייצר כמה אירועים - למשל שינוי סטטוס ושיוך חדש באותו עדכון.

## אימות החתימה

`X-Banta-Signature` הוא `sha256=` ואחריו ה-HMAC-SHA256 בהקסדצימלי (אותיות קטנות) של `<X-Banta-Timestamp>.<גוף הבקשה הגולמי>`, עם הסוד **כולו** כמפתח (כולל הקידומת `whsec_`).

- אמתו מול גוף הבקשה הגולמי, בדיוק כפי שהתקבל - לא JSON שעבר סריאליזציה מחדש.
- דחו חותמות זמן שרחוקות ביותר מ-5 דקות מהשעון שלכם.
- השוו בזמן קבוע (constant time).

**Node.js (Express)**

```js
import crypto from 'node:crypto'
import express from 'express'

const SECRET = process.env.BANTA_WEBHOOK_SECRET // whsec_... (the whole value)
const app = express()

app.post('/banta/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-Banta-Timestamp') ?? ''
  const signature = req.get('X-Banta-Signature') ?? ''
  const rawBody = req.body.toString('utf8')

  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return res.status(400).send('stale timestamp')
  }
  const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(`${timestamp}.${rawBody}`).digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(signature)
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send('invalid signature')
  }

  const event = JSON.parse(rawBody)
  // event.id === X-Banta-Event-Id - skip it if you already processed it
  res.sendStatus(200) // reply fast, process asynchronously
})
```

**Python (Flask)**

```python
import hashlib, hmac, os, time
from flask import Flask, abort, request

SECRET = os.environ["BANTA_WEBHOOK_SECRET"].encode()  # whsec_... (the whole value)
app = Flask(__name__)

@app.post("/banta/webhooks")
def banta_webhook():
    timestamp = request.headers.get("X-Banta-Timestamp", "")
    signature = request.headers.get("X-Banta-Signature", "")
    raw_body = request.get_data()  # raw bytes, exactly as sent

    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        abort(400)
    expected = "sha256=" + hmac.new(SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature):
        abort(401)

    event = request.get_json()
    # deduplicate on event["id"]
    return "", 200
```

**PHP**

```php
<?php
$secret    = getenv('BANTA_WEBHOOK_SECRET'); // whsec_... (the whole value)
$timestamp = $_SERVER['HTTP_X_BANTA_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_BANTA_SIGNATURE'] ?? '';
$rawBody   = file_get_contents('php://input');

if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(400);
    exit;
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($rawBody, true);
// deduplicate on $event['id']
http_response_code(200);
```

## תגובה וניסיונות חוזרים

החזירו `2xx` כלשהו בתוך **10 שניות**, ובצעו את העבודה באופן אסינכרוני.

| התגובה שלכם | מה banta עושה |
|---|---|
| `2xx` | נמסר |
| `408`, `429`, `5xx`, שגיאת רשת או timeout | ניסיונות חוזרים אחרי דקה, 5 דקות, 30 דקות, 2, 6, 12 ו-24 שעות - 8 ניסיונות בתוך כ-45 שעות. אחר כך המשלוח עובר ל-`parked` |
| כל `4xx` אחר, או הפניה (`3xx`) | סופי - אין ניסיון חוזר. הפניות לעולם לא נעקבות |
| `410 Gone` | נקודת הקצה מושבתת מיד (כך Zapier מבטל הרשמה) |

אחרי **30 ניסיונות כושלים ברציפות** נקודת הקצה מושבתת, המשלוחים שממתינים לה מדולגים, ומנהלי הארגון מקבלים התראה ב-banta. תקנו את השרת המקבל ואז הפעילו אותה מחדש עם `PATCH /webhooks/{id}` ו-`{"is_active": true}` - רק אירועים שקורים אחרי זה נשלחים.

- **המשלוח מובטח לפחות פעם אחת.** לניסיון חוזר ולשליחה חוזרת יש אותו `id` (ואותו `X-Banta-Event-Id`) - השתמשו בו כדי להתעלם מכפילויות.
- **הסדר אינו מובטח.** השתמשו ב-`created_at` ובתצלום, ושלפו את הרשומה כשהסדר חשוב.
- אם מפתח ה-API שיצר נקודת קצה נמחק או בוטל, גם נקודת הקצה מושבתת.

## יומן המשלוחים

`GET /webhooks/{id}/deliveries` מחזיר את המשלוחים של נקודת קצה, מהחדש לישן - סטטוס, מספר ניסיונות, קוד ה-HTTP והשגיאה האחרונים, וה-payload. `POST /webhooks/{id}/deliveries/{deliveryId}/redeliver` שולח משלוח שוב עם אותו `id` של האירוע. היומן נשמר 14 יום (משלוחים שנמסרו) או 30 יום (כל השאר).

## Zapier ו-Make

נקודות הקצה תואמות לדפוס REST Hooks: נרשמים עם `POST /webhooks` ושומרים את ה-`id` שחוזר; מבטלים הרשמה עם `DELETE /webhooks/{id}`. ב-Make או ב-n8n יוצרים webhook מותאם, מעתיקים את הכתובת שלו ורושמים אותה באותה דרך.

---

חלק מתיעוד ה-API של banta. אינדקס מלא: https://docs.banta.co.il/he/llms.txt
