דילוג לתוכן
רצף – הפקת מסמכים

תיעוד API למפתחים

כל מה שיש במסכים יש גם ב-API: עסקים, מסמכים, מקור והעתק, עיצובים, webhooks וחיבור ל-Claude. התיעוד נבנה ישירות מ-engine/docs/API.md של המנוע.

כתובת הבסיס

https://create-my-repo-06.lovable.app/api/v1

בתיעוד הנתיבים כתובים כ-/v1/..., ובאתר שולחים אותם תחת /api. בדוגמאות, https://<engine> הוא https://create-my-repo-06.lovable.app/api.

הרשאה

בכל קריאה Authorization: Bearer <token>, חוץ מהרשמה וכניסה.

  • ies_ כניסת משתמש, 12 שעות
  • iep_ טוקן גישה אישי, עד שנה
  • iek_ מפתח API של עסק, רק בצד השרת

כללי יסוד

  • סכומים באגורות (מספר שלם, 100 = 1 ₪), תאריכים YYYY-MM-DD.
  • מסמך שהופק לא נמחק ולא משתנה; תיקון רק בזיכוי (330).
  • לפני הפקה: "dryRun": true ואישור מפורש של המשתמש.
  • שגיאות: { "error": "..." } בעברית.
התחלה מהירה
מהרשמה ועד מקור חתום בחמש קריאות. מגדירים פעם אחת את כתובת הבסיס:
Shell
BASE=https://create-my-repo-06.lovable.app/api
  1. 1

    הרשמה או כניסה

    מקבלים טוקן כניסה (ies_), בתוקף 12 שעות. משתמש עם אימות דו-שלבי שולח גם code בכניסה.

    Shell
    curl -s -X POST "$BASE/v1/auth/signup" -H "Content-Type: application/json" \
      -d '{"email":"me@example.com","name":"ישראל ישראלי","password":"<10+ chars>","acceptTerms":true}'
    # או כניסה למשתמש קיים:
    curl -s -X POST "$BASE/v1/auth/login" -H "Content-Type: application/json" \
      -d '{"email":"me@example.com","password":"<password>"}'
    # → { "token": "ies_...", "user": {...}, "expiresAt": "..." }
    TOKEN=ies_...
  2. 2

    טוקן לחיבור (לא חובה)

    לחיבור שרץ לאורך זמן: טוקן גישה אישי (iep_) עד שנה, או מפתח API של עסק (iek_) בהגדרות העסק. רק בכניסה עם סיסמה, ועם הסיסמה שוב בגוף הבקשה.

    Shell
    curl -s -X POST "$BASE/v1/me/tokens" -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name":"CRM","expiresInDays":90,"currentPassword":"<password>"}'
    # → 201 { "token": "iep_...", ... }  (מוצג פעם אחת בלבד)
  3. 3

    רישום עסק

    בפעולה אחת: פרטים, כל הסדרות שמותרות לסוג העסק (עם מספרי התחלה), מפתח חתימה ותאריך ההודעה לפקיד השומה.

    Shell
    curl -s -X POST "$BASE/v1/businesses/register" -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name":"העסק שלי","vatNumber":"123456782","kind":"licensed","city":"תל אביב",
           "firstNumbers":{"305":1,"320":1,"330":1,"400":1},"computerizedNoticeSentOn":"2026-10-01"}'
    # → 201 { "businessId": "...", "sequences": [...], "certificatePem": "..." }
    BIZ=<businessId>
  4. 4

    בדיקה בלי הפקה, ואז הפקה

    סכומים באגורות (90000 = 900 ₪). עם "dryRun": true שום דבר לא נשמר. אחרי אישור מפורש – אותה בקשה בלי dryRun.

    Shell
    curl -s -X POST "$BASE/v1/businesses/$BIZ/documents" -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"docType":320,"dryRun":true,"customer":{"name":"משפחת כהן"},
           "lines":[{"description":"הפעלה","quantity":1,"unitPrice":90000}],"pricesIncludeVat":true,
           "payments":[{"method":4,"amount":90000,"paymentDate":"2026-10-30"}]}'
    # → 200 { "dryRun": true, "docNumber": 1, "amountTotal": 90000, ... }
    # אחרי אישור: אותו גוף בלי "dryRun" → 201 { "id": 431, "docNumber": 1, ... }
    DOC=431
  5. 5

    מקור

    המקור מופק פעם אחת. computerized: true = מקור ממוחשב חתום (דורש הודעה לפקיד השומה והסכמת הלקוח); false = מקור להדפסה.

    Shell
    curl -s -X POST "$BASE/v1/documents/$DOC/original" -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" -d '{"computerized":false}' -o original.pdf
    # העתק בכל עת:
    curl -s "$BASE/v1/documents/$DOC/copy" -H "Authorization: Bearer $TOKEN" -o copy.pdf
Claude ו-MCP
אותו מנוע כשרת MCP: Claude מכין, מציג לבדיקה ומפיק רק אחרי אישור מפורש.
https://create-my-repo-06.lovable.app/api/mcp
איך מחברים את Claude

התיעוד המלאAPI של מנוע ההפקה

כל פעולה שיש במסכים יש גם ב-API: הרשמת עסק, הפקת כל סוגי המסמכים (כולל חשבונית זיכוי וקבלה על חשבונית), מספר הקצאה, מקור והעתק, הסכמות, מבנה אחיד, סקירת העסק ויומן הפעולות על כל מסמך. המסכים עצמם (ההרשמה והאזור האישי) עושים בדיוק את אותן פעולות מול אותו מנוע.

כל הקריאות תחת /v1 דורשות כותרת Authorization: Bearer <token>, חוץ מ-/v1/auth/login ו-/v1/auth/signup. יש שישה סוגי טוקנים:

טוקןמה מותר
כניסת משתמש (ies_..., מ-/v1/auth/login)לפי התפקיד של המשתמש בכל עסק שהוא שייך אליו (טבלה בהמשך). בתוקף 12 שעות
טוקן גישה אישי (iep_..., מ-POST /v1/me/tokens)בדיוק כמו כניסת המשתמש שיצר אותו, לחיבורים כמו Claude / MCP. בתוקף עד שנה (ברירת מחדל 90 יום). לא יוצר טוקנים, מפתחות API או הזמנות לעסק, לא משנה סיסמה ולא אימות דו-שלבי
מפתח API של עסק (iek_...)צפייה והפקה בעסק שלו בלבד: חשבוניות, זיכויים, קבלות, מקור, העתק, הסכמות, מבנה אחיד. לא משנה הגדרות, משתמשים או מפתחות. עסק אחר מחזיר 403
טוקן שירות (ENGINE_API_TOKENS, למשל של ה-CRM)צפייה והפקה בכל העסקים, בלי שינוי הגדרות
טוקן מנהלהכול, כולל הרשמת עסקים בשם אחרים, איפוס סיסמאות ומספרי התחלה, הענקת ניהול מערכת למשתמש, והמעבר של עסק למצב מסמכים אמיתיים (גם מנהל פלטפורמה, מהאתר). ב-webhooks: צפייה, השהיה, צמצום, מחיקה, בדיקה ושליחה חוזרת, אבל לא יצירה, הפעלה מחדש או הרחבה (ראו Webhooks)
טוקן המשגר של ה-webhooks (ENGINE_WEBHOOK_DISPATCH_TOKEN / Vault invoice_engine_dispatch_token)רק /v1/internal/webhooks/claim, /report ו-/routines, בדרך כלל כאישור קצר-מועד whd1 (2 דקות). לא תקף בשום נתיב אחר, ושום טוקן אחר לא תקף בהם

מה אפשר לעשות עם טוקן המשגר (או עם אישור whd1 תקף), אם הוא דולף: לקרוא את משלוחי ה-standard שממתינים בכל העסקים (גוף ההודעות, החתימות והכתובות המלאות של המקבלים, כולל הפרמטרים שבהן), לדווח שמשלוח נשלח כשלא נשלח, לעכב משלוחים, ואחרי 3 דיווחים במשך 10 דקות לפחות – להשבית webhook. מה אי אפשר: לקרוא סודות חתימה או טוקנים של routines, להפעיל routines של Claude בעצמו (המנוע שולח אותם, והטוקן לא יוצא ממנו), או לגשת לכל נתיב אחר. החלפה: ב-Supabase: select vault.update_secret(id, encode(extensions.gen_random_bytes(32), 'hex')) from vault.secrets where name = 'invoice_engine_dispatch_token'; (ה-Edge Function קורא את הערך החדש תוך 60 שניות, ו-pg_net מפיק ממנו אישורים מיד). בשרת ה-Node: משנים את משתנה הסביבה ומפעילים מחדש.

תפקידים בעסק:

תפקידצפייה, העתק, מבנה אחידהפקה, זיכוי, קבלה, מקור, מספר הקצאה, הסכמותהגדרות העסק, משתמשים, מפתחות API, מפתח חתימה
owner בעל העסק✅✅✅
staff עובד✅✅—
accountant רואה חשבון✅——

מפתחות API ומשתמשים מנהל בעל העסק (או המנהל). מפתח API מוצג פעם אחת בלבד, ובבסיס הנתונים נשמר רק ה-SHA-256 שלו. כל פעולה על מסמך נרשמת ביומן בשם מי שביצע אותה (אימייל המשתמש, או שם המפתח). פעולה בטוקן גישה אישי נרשמת כאימייל ושם הטוקן, למשל nissim@example.com (token Claude).

משתמשים וכניסה

קריאהגוףתוצאה
POST /v1/auth/signup{ email, name, password, acceptTerms: true }201 { token, user, expiresAt }. רק כשהשרת פתוח להרשמה עצמית (ENGINE_ALLOW_SIGNUP=true). גרסת התנאים שאושרה נשמרת
POST /v1/auth/login{ email, password, code? }{ token, user, expiresAt }. אחרי 5 ניסיונות כושלים הכניסה נחסמת לרבע שעה (429). משתמש עם אימות דו-שלבי: בלי code (או עם קוד שגוי) מתקבל 401 עם twoFactorRequired: true
POST /v1/auth/logout—הטוקן שבבקשה מפסיק לעבוד (כניסה, או טוקן גישה אישי)
GET /v1/me—{ user, expiresAt, twoFactorEnabled, platformAdmin, businesses: [{ id, name, vatNumber, kind, role }] }. בטוקן גישה אישי גם token: { id, name }, ו-expiresAt הוא תוקף הטוקן. platformAdmin (מנוע 0.14) הוא true רק למנהל פלטפורמה בכניסה עם סיסמה (בטוקן גישה אישי תמיד false), ראו ניהול מערכת
PUT /v1/me/password{ currentPassword, newPassword }כל הכניסות וכל טוקני הגישה האישיים של המשתמש מתבטלים. רק בכניסה עם סיסמה
PUT /v1/users/password (מנהל){ email, newPassword }איפוס סיסמה. גם כאן כל הכניסות וכל טוקני הגישה מתבטלים
POST /v1/me/two-factor—{ secret, otpauthUrl } להגדרת אפליקציית האימות
POST /v1/me/two-factor/confirm{ code }{ recoveryCodes } – 8 קודי שחזור, מוצגים פעם אחת. מכאן הכניסה דורשת קוד
DELETE /v1/me/two-factor{ code }כיבוי, בקוד תקף
DELETE /v1/users/two-factor (מנהל){ email }כיבוי למשתמש שאיבד את הטלפון ואת קודי השחזור, אחרי זיהוי מחוץ למערכת

סיסמה: 10 תווים לפחות, לא רק מספרים, ולא שם המשתמש מהאימייל. נשמר רק hash מסוג PBKDF2-SHA256 (600,000 חזרות) עם salt אקראי. את אימות הדו-שלבי (שלוש הקריאות של /v1/me/two-factor), שינוי הסיסמה, יצירת טוקן גישה אישי, יצירת מפתח API לעסק והזמנה לעסק אפשר לעשות רק בכניסה עם סיסמה, לא בטוקן גישה אישי (403).

טוקני גישה אישיים (למשל לחיבור Claude / MCP)

משתמש יוצר טוקן לחיבור חיצוני, והחיבור פועל בשמו: אותם עסקים ואותן הרשאות לפי התפקיד בכל עסק (רואה חשבון בטוקן עדיין לא מפיק מסמכים). כשהמשתמש מוסר מעסק, גם הטוקן מאבד את הגישה אליו. הטוקן מוצג פעם אחת בלבד, ובבסיס הנתונים נשמר רק ה-SHA-256 שלו. שולחים אותו כמו כל טוקן: Authorization: Bearer iep_....

טוקן שדלף נגמר כשמבטלים אותו, כשמשנים סיסמה או כשמנהל מבטל את הטוקנים של המשתמש. לכן הוא לא יכול ליצור גישה שנשארת אחריו: לא טוקן חדש, לא מפתח API לעסק (POST /v1/businesses/:id/api-keys) ולא הזמנה לעסק (POST /v1/businesses/:id/invitations). את אלה עושים רק בכניסה עם סיסמה (בטוקן מתקבל 403). לבטל מפתח או הזמנה מותר גם בטוקן. מאותה סיבה טוקן לא יוצר webhook, לא מפעיל מחדש webhook מושבת, לא מוסיף לו סוגי אירועים, לא משנה את הכתובת שלו או את הטוקן של ה-routine, לא מחליף את סוד החתימה ולא מציג את הכתובת המלאה (כל אלה דורשים גם את הסיסמה שוב בגוף הבקשה), ולא שולח בדיקה או שליחה חוזרת ל-routine של Claude (כל אחת פותחת שיחה בחשבון של בעל העסק). להשהות, לצמצם, למחוק, לבדוק webhook רגיל ולשלוח שוב משלוח שנכשל – מותר גם בטוקן.

קריאהגוףתוצאה
POST /v1/me/tokens{ name, expiresInDays?, currentPassword, code? }201 { id, token, tokenPrefix, name, createdAt, expiresAt }. את token שומרים מיד; הוא לא יוצג שוב. name עד 100 תווים; expiresInDays מספר שלם 1 עד 365, ברירת מחדל 90. רק בכניסה עם סיסמה (ies_): בטוקן גישה מתקבל 403, כך שטוקן שדלף לא יוצר טוקנים חדשים. ועם אימות מחדש: currentPassword היא הסיסמה של המשתמש, ו-code קוד מאפליקציית האימות (או קוד שחזור) כשיש אימות דו-שלבי, כך שכניסה פתוחה שנגנבה לא הופכת לטוקן לשנה. סיסמה שגויה: 401; חסר קוד או קוד שגוי: 401 עם twoFactorRequired: true. כישלונות נספרים כמו בכניסה: אחרי 5, חסימה לרבע שעה (429). קלט שגוי (שם, תוקף) נבדק קודם ומחזיר 422 בלי להיספר
GET /v1/me/tokens—{ tokens: [{ id, name, tokenPrefix, createdAt, expiresAt, lastUsedAt, revokedAt }] } – כל הטוקנים של המשתמש, גם שפגו או בוטלו, בלי הטוקנים עצמם. lastUsedAt מתעדכן לכל היותר פעם בדקה
DELETE /v1/me/tokens/:tokenId—ביטול טוקן של המשתמש עצמו (אפשר גם מתוך טוקן). הוא מפסיק לעבוד מיד, והרשומה נשארת. טוקן שלא נמצא או שכבר בוטל: 404
DELETE /v1/users/tokens (מנהל){ email }{ ok, revoked } – ביטול כל הטוקנים של המשתמש, למשל כשטוקן דלף

שינוי סיסמה (או איפוס על ידי מנהל) מבטל את כל הטוקנים של המשתמש, כמו את הכניסות.

משתמשים בעסק (בעל העסק)

קריאהתוצאה
GET /v1/businesses/:id/members{ members: [{ userId, email, name, role }], invitations: [...] }
POST /v1/businesses/:id/invitations עם { email, role }201 { token, ... }. את הקישור /invite/<token> שולחים למוזמן. בתוקף שבוע, לשימוש אחד. רק בכניסה עם סיסמה, לא בטוקן גישה אישי (403)
DELETE /v1/businesses/:id/invitations/:invitationIdביטול הזמנה
DELETE /v1/businesses/:id/members/:userIdהסרה מהעסק. בעל העסק האחרון לא מוסר
GET /v1/invitations/:token (משתמש מחובר)שם העסק, האימייל שהוזמן והתפקיד
POST /v1/invitations/:token/accept (משתמש מחובר)הצטרפות. רק המשתמש עם האימייל שהוזמן

הסכומים הם באגורות (מספר שלם), והתאריכים בפורמט YYYY-MM-DD. השגיאות חוזרות כ-JSON בצורה { "error": "..." }:

קודמשמעות
400בקשה לא תקינה
401חסר טוקן, או שהטוקן שגוי
403אין הרשאה לפעולה (למשל רק למנהל, תפקיד בלי הרשאה, עסק אחר, או פעולה שדורשת כניסה עם סיסמה)
404לא נמצא
409ניסיון לשנות את הקובץ הקבוע, או לשנות את המספור של סדרה שכבר הופקו בה מסמכים
422המנוע סירב, וההודעה בעברית מסבירה למה (למשל "חשבונית מס/קבלה מחייבת תשלום מלא")

בריאות

GET /health → { "ok": true, "software": "...", "version": "...", "registrationNumber": "...", "build": "<sha256>" } (בלי טוקן). build היא טביעת המהדורה שרצה בשרת, כדי לוודא שזו המהדורה שנרשמה (ראו EDITIONS.md).

הרשמת עסק

קריאה אחת שעושה את כל מה שעושה מסך ההרשמה, בטרנזקציה אחת (אם שלב נכשל, לא נשאר עסק חצי רשום). משתמש מחובר נעשה בעל העסק (כשהשרת פתוח להרשמה עצמית). מנהל יכול לצרף בעל עסק עם "owner": { "email", "name", "password" }; אם האימייל כבר רשום, המשתמש הקיים מצורף.

POST /v1/businesses/register

JSON
{
  "name": "חויה בקליק",
  "vatNumber": "316288877",
  "kind": "licensed",
  "street": "שמואל הנביא", "houseNumber": "7", "city": "בני ברק", "phone": "054-...", "email": "...",
  "firstNumbers": { "305": 60001, "320": 50001, "330": 80001, "400": 70001 },
  "computerizedNoticeSentOn": "2026-10-01"
}
  • נפתחות כל הסדרות שמותרות לסוג העסק: עוסק מורשה וחברה 305, 320, 330, 400; עוסק פטור 300, 400; עמותה 400. סדרה שלא צוינה ב-firstNumbers מתחילה ב-1. סדרה שאסורה לסוג העסק נדחית. מספר התחלה הוא מספר שלם מ-1 עד 9,999,999,999 (10 ספרות). כל עוד לא הופק בסדרה מסמך, אפשר להעלות אותו אחר כך (שינוי מספר ההתחלה).
  • נוצרים מפתח ותעודת חתימה לעסק.
  • computerizedNoticeSentOn לא חובה. בלעדיו אפשר להפיק רק מקור להדפסה, עד שרושמים את התאריך.

התשובה: 201 { businessId, sequences: [{ docType, firstNumber }], certificatePem }.

סקירת העסק

GET /v1/businesses/:id מחזיר את מה שמוצג במסך הסקירה באזור האישי:

שדהתוכן
businessפרטי העסק, כולל computerizedNoticeSentOn
allowedDocTypesסוגי המסמכים שהעסק רשאי להפיק
sequences[{ docType, nextNumber, issued }]: המספר הבא בכל סדרה וכמה מסמכים הופקו בה. כל עוד issued הוא 0 אפשר להעלות את המספר הבא (שינוי מספר ההתחלה)
signingCertificateשם, מספר עוסק, תוקף וטביעת אצבע SHA-256 של תעודת החתימה, וה-PEM
computerizedReadyאפשר להפיק מקור ממוחשב חתום (יש מפתח, ונרשמה הודעה לפקיד השומה)
month, yearמספר מסמכים, הכנסות לפני מע"מ (חשבוניות פחות זיכויים) ותקבולים
pendingחשבוניות שחסר להן מספר הקצאה, תקבולים שמחכים למקור מודפס לחתימת יד, ומספר המסמכים שעוד לא הופק להם מקור
realDocumentsSinceמתי העסק עבר למצב מסמכים אמיתיים (ISO), או null במצב הדגמה
lastArchiveExportAtמתי הורד הייצוא המלא האחרון (ISO), או null כשעוד לא הורד. לתזכורת הגיבוי הרבעונית
designs(מנוע 0.13) { count, active, defaultId, defaultName }: כמה עיצובים יש לעסק, כמה פעילים (לא בארכיון), ועיצוב ברירת המחדל

גם GET /v1/businesses מחזיר לכל עסק את real_documents_since (או null).

שינוי מספר ההתחלה של סדרה

עסק שנרשם בלי firstNumbers (או עם מספר לא נכון) יכול לקבוע את המספר הבא של סדרה, כל עוד לא הופק בעסק אף מסמך מאותו סוג. למשל כשממשיכים ממערכת אחרת (סאמיט וכדומה) באמצע השנה.

PUT /v1/businesses/:id/sequences/:docType עם { "nextNumber": 60001 } → 200 { docType, nextNumber, previousNextNumber }. המסמך הראשון מהסוג הזה יקבל את nextNumber.

  • רק כלפי מעלה: nextNumber חייב להיות גדול מהמספר הבא הנוכחי (sequences[].nextNumber בסקירת העסק). אי אפשר להוריד מספר, גם לא חזרה למספר שהיה לפני שינוי קודם (הטריגר של בסיס הנתונים לא מאפשר מונה שחוזר אחורה).
  • רק לפני המסמך הראשון מהסוג הזה בעסק. מסמכים מסוג אחר, או בדיקה בלי הפקה (dryRun), לא חוסמים. אחרי מסמך אחד – 409. אפשר לשנות כמה פעמים לפני המסמך הראשון, וכל פעם רק למעלה.
  • מספר שלם מ-1 עד 9,999,999,999 (10 ספרות). המספר נשמר כ-bigint ומודפס כמו שהוא, ובמבנה האחיד יש לו 20 תווים; המגבלה משאירה מקום לכל המסמכים שאחריו ותופסת טעות הקלדה (אפס מיותר), שאחריה אי אפשר היה לחזור למספר נמוך.
  • מי: בעל העסק או מנהל. מותר גם בטוקן גישה אישי (iep_), כי השינוי לא נותן גישה שנשארת אחרי הטוקן. לא במפתח API של עסק, לא בטוקן שירות, ולא לעובד או לרואה חשבון (403).
  • יומן: כל שינוי נרשם (המספר הבא לפני ואחרי, מי ומתי), ואת הרשומות אי אפשר לשנות או למחוק. הן נכללות גם בייצוא המלא (sequence-changes.csv).
  • מקביליות: השינוי וההפקה נועלים את העסק ואת הסדרה באותו סדר, ולכן רצים אחד אחרי השני: אם ההפקה נשמרה קודם, השינוי נדחה ב-409; אם השינוי נשמר קודם, המסמך מקבל את המספר החדש.
קודמתי
400docType לא תקין, או nextNumber שאינו מספר (JSON number)
403אין הרשאה (ראו למעלה)
404העסק לא נמצא, או שלא נפתחה סדרה לסוג הזה (פותחים אותה ב-POST /v1/businesses/:id/sequences עם המספר הרצוי)
409"כבר הופקו מסמכים מסוג זה בעסק; אי אפשר לשנות את המספור"
422המספר לא גדול מהמספר הבא הנוכחי, לא מספר שלם חיובי, או יותר מ-10 ספרות

GET /v1/businesses/:id/sequences/changes (כל מי שיש לו גישת צפייה בעסק: בעל העסק, עובד, רואה חשבון, מפתח API של העסק, מנהל) → { changes: [{ id, docType, previousNextNumber, newNextNumber, actor, at }] }, מהישן לחדש. actor הוא אימייל המשתמש (בטוקן גישה אישי גם שם הטוקן, למשל nissim@example.com (token Claude)) או admin.

מעבר ממערכת אחרת: לכל סוג מסמך בנפרד קובעים המספר הבא = המספר האחרון שהופק במערכת הקודמת מאותו סוג + 1 (חשבונית מס, חשבונית מס/קבלה, זיכוי, קבלה וכו'), כך שהמספור ממשיך ברצף ולא חוזר על מספר שכבר הונפק באותה שנה (הוראה 18(א)(3)). את המספר האחרון בודקים במערכת הקודמת אחרי שמפסיקים להפיק בה. אף פעם לא מפיקים את אותו סוג מסמך בשתי המערכות במקביל: מפסיקים במערכת הקודמת, קובעים כאן את המספר הבא, ורק אז מפיקים כאן. מסמך שהופק במערכת הקודמת אחרי שנקבע המספר כאן היה מקבל מספר שהמערכת הזו תיתן שוב.

הקמה ידנית, צעד אחר צעד (מנהל)

קריאהגוףתוצאה
POST /v1/businesses{ name, vatNumber, kind: "licensed"|"exempt"|"company"|"nonprofit", street?, houseNumber?, city?, zip?, phone?, email? }201 { id }
PUT /v1/businesses/:idשדות לעדכון (למעט מספר עוסק), כולל section46Approval ו-section46ValidUntil לעמותהבעל העסק או מנהל. מסמכים קיימים שומרים את הפרטים שהיו בזמן ההפקה
POST /v1/businesses/:id/sequences{ docType, firstNumber }פתיחת סדרה, פעם אחת לכל סדרה. בעל העסק או מנהל
PUT /v1/businesses/:id/sequences/:docType{ nextNumber }שינוי המספר הבא, רק למעלה ורק לפני המסמך הראשון (שינוי מספר ההתחלה)
GET /v1/businesses/:id/sequences/changes—יומן שינויי מספר ההתחלה
POST /v1/businesses/:id/signing-keys—201 { certificatePem }. בעל העסק או מנהל. החלפת מפתח: מסמכים ישנים נשארים חתומים במפתח הקודם
PUT /v1/businesses/:id/computerized-notice{ sentOn }הוראה 18ב(ב). בעל העסק או מנהל
GET /v1/businesses—רשימת העסקים. משתמש רואה את העסקים שלו, ומפתח של עסק רק את העסק שלו
PUT /v1/businesses/:id/real-documents{ enabled, note? }רק מנהל: מצב מסמכים אמיתיים (פירוט)

ייצוא מלא

GET /v1/businesses/:id/archive (כל מי שיש לו גישת צפייה) מחזיר ZIP עם:

  • originals/ – כל המקורות כפי שנמסרו ללקוחות, כולל החתימה הדיגיטלית;
  • openformat/<שנה>/ – קובץ מבנה אחיד לכל שנת מס;
  • documents.csv, consents.csv, events.csv – רשימת המסמכים, הסכמות הלקוחות ויומן הפעולות;
  • sequence-changes.csv – שינויי מספר ההתחלה של סדרות (לפני המסמך הראשון בכל סדרה);
  • mode-changes.csv – המעברים בין מצב הדגמה למצב מסמכים אמיתיים (ב-documents.csv העמודה "מסמך אמיתי": כן, או לא (הדגמה) למסמך שהופק במצב הדגמה);
  • designs.csv – (מנוע 0.13) העיצובים של העסק: מזהה, שם, ברירת מחדל, ארכיון והגרסה הנוכחית. ב-documents.csv העמודות "עיצוב" (מזהה העיצוב) ו"גרסת עיצוב" אומרות באיזה עיצוב ובאיזו גרסה שלו הופק כל מסמך;
  • certificates/ – תעודות החתימה, לאימות החתימות בעתיד;
  • business.json, README.txt.

אפשר להוריד בכל עת, וגם בסיום השימוש, כדי לשמור את הרשומות 7 שנים (סעיף 25).

יומן הייצואים (מנוע 0.12): כל ייצוא מלא שהמנוע מחזיר נרשם בטבלה archive_exports (העסק, מי – actor כמו ביומן הפעולות, מתי ומספר המסמכים בקובץ). אי אפשר לשנות או למחוק רשומה. בסקירת העסק lastArchiveExportAt הוא הייצוא האחרון. לפי הוראה 25(ו)(2) הגיבוי נעשה בשבוע הראשון של כל רבעון, ונשמר בישראל במקום אחר מהמקום שבו מוחזקת המערכת; האתר מציג לבעל העסק ולרואה החשבון תזכורת כשלא הורד ייצוא מלא מתחילת הרבעון הנוכחי (שעון ישראל).

מצב מסמכים אמיתיים (מנהל)

בסביבה שמסמנת כל PDF בסימן מים (למשל סביבת ההדגמה: "הדגמה בלבד – אינו מסמך חשבונאי"), מנהל המנוע יכול להעביר עסק מסוים למצב מסמכים אמיתיים. רק טוקן המנהל, או מפעיל עם גישה לבסיס הנתונים: לא בעל העסק, לא טוקן גישה אישי, לא מפתח API, לא טוקן שירות, ולכן גם לא כלי ה-MCP (כולם מקבלים 403). ממנוע 0.14 גם מנהל פלטפורמה מהאתר, בנתיב נפרד עם אימות מחדש (ראו ניהול מערכת); הנתיב כאן נשאר לטוקן המנהל בלבד.

PUT /v1/businesses/:id/real-documents עם { "enabled": true, "note": "למה" } (note לא חובה, עד 1,000 תווים) → 200 { realDocuments, realDocumentsSince, changed, changes: [{ id, realDocuments, actor, note, at }] }. enabled: false מחזיר את העסק למצב הדגמה. מעבר למצב שהעסק כבר נמצא בו לא משנה דבר ולא נרשם (changed: false). קודים: 400 (enabled שאינו true/false, note שאינו טקסט או ארוך מדי, מזהה עסק לא תקין), 403 (לא טוקן המנהל), 404 (העסק לא נמצא).

מפעיל עם גישה לבסיס הנתונים בלבד (למשל SQL Editor של Lovable Cloud, שבו טבלאות המנוע בסכימה invoice):

SQL
select * from invoice.set_real_documents('<מזהה העסק>'::uuid, true, '<מי מבצע>', '<למה>');
-- חזרה למצב הדגמה
select * from invoice.set_real_documents('<מזהה העסק>'::uuid, false, '<מי מבצע>', '<למה>');

הפונקציה (מיגרציה 013) נועלת את שורת העסק, מעדכנת את businesses.real_documents_since (זמן המעבר, או null) ורושמת את המעבר ביומן business_mode_changes (מצב, מי, הערה, מתי), הכול בטרנזקציה אחת; ה-API קורא לאותה פונקציה. ביומן אי אפשר לשנות או למחוק רשומה, והוא נכלל בייצוא המלא (mode-changes.csv). הפונקציה רצה בהרשאות מי שקורא לה (לא security definer), וההרשאה ל-PUBLIC הוסרה ממנה.

מה משתנה במסמכים:

  • כל מסמך מקבל בהפקה את הסימון documents.real_document לפי המצב של העסק באותו רגע (באותה טרנזקציה, כשהעסק נעול), והסימון לא משתנה לעולם. מסמך שהופק לפני המעבר נשאר מסמך הדגמה, וכל העתק שלו ממשיך לצאת עם סימן המים. מסמך שהופק במצב מסמכים אמיתיים נשאר אמיתי גם אם העסק יחזור למצב הדגמה.
  • במסמך אמיתי (מקור, העתק, ובדיקה בלי הפקה): אין סימן מים, והשורה התחתונה בכל עמוד היא בדיוק הופק על ידי העסק (במקום הופק באמצעות <שם התוכנה> <גרסה> | מספר רישום תוכנה: …). מספר העמוד וטביעת המסמך נשארים. מסמך הדגמה – בלי שינוי. תצוגת עיצוב לדוגמה (POST /v1/businesses/:id/design/preview) אינה מסמך ונשארת כמו שהייתה.
  • realDocument (true/false) מוחזר בתשובת ההפקה, ב-GET /v1/documents/:docId ובבדיקה בלי הפקה.
  • הסימון אינו חלק מ-contentHash: ה-hash מכסה את התוכן החשבונאי של המסמך ואת השרשרת, והסימון קובע רק איך הסביבה מציגה אותו. העמודה חסומה לשינוי כמו כל שורת המסמך, ולכן אין צורך להגן עליה גם ב-hash, וה-hash של מסמכים קיימים נשאר כפי שחושב.

ניהול מערכת (מנהלי פלטפורמה)

ממנוע 0.14 (מיגרציה 015) משתמש רגיל יכול להיות מנהל פלטפורמה (users.platform_admin): בעל התוכנה מנהל מהאתר (דף /admin) את מה שעד עכשיו רק טוקן המנהל יכול היה לעשות – רשימת כל העסקים והמעבר של עסק למצב מסמכים אמיתיים.

מה זה נותן, ומה לא:

  • רק את הנתיבים /v1/platform/* שבטבלה. מנהל פלטפורמה לא מקבל גישה למסמכים, ללקוחות, להגדרות או למשתמשים של עסק שהוא לא חבר בו: שם ממשיכים לבדוק את התפקיד שלו בעסק (403 כמו לכל משתמש). הרשימה מציגה רק נתונים מסכמים.
  • רק בכניסה עם אימייל וסיסמה (ies_). בטוקן גישה אישי (iep_), ולכן גם בכלי ה-MCP ובחיבור ל-Claude, במפתח API ובטוקן שירות – 403. אין כלי MCP לנתיבים האלה.
  • המעבר עצמו דורש אימות מחדש בגוף הבקשה, כמו ביצירת טוקן גישה אישי: currentPassword, ו-code (מאפליקציית האימות, או קוד שחזור) כשיש אימות דו-שלבי. סיסמה שגויה: 401; חסר קוד או קוד שגוי: 401 עם twoFactorRequired: true. כישלונות נספרים כמו בכניסה: אחרי 5, חסימה לרבע שעה (429). קלט שגוי נבדק קודם ומחזיר 400 בלי להיספר.
  • הסימון נקרא מחדש בכל בקשה: ביטול נכנס לתוקף מיד, גם בכניסה שכבר פתוחה.
  • טוקן המנהל עובד גם בנתיבים האלה (בלי אימות מחדש, ונרשם admin).
קריאהגוףתוצאה
GET /v1/platform/businesses?q=&limit=&offset=—{ businesses: [{ id, name, vatNumber, kind, createdAt, realDocumentsSince, ownerEmails, documents, realDocuments, lastDocumentDate, lastIssuedAt }], total, limit, offset }. מהחדש לישן. q (עד 100 תווים) מחפש בשם העסק, במספר העוסק ובאימייל של בעל העסק; limit 1 עד 200 (ברירת מחדל 50), offset מ-0
PUT /v1/platform/businesses/:id/real-documents{ enabled, note, currentPassword, code? }200 { realDocuments, realDocumentsSince, changed, business, changes } – אותה פונקציה (set_real_documents) ואותו יומן כמו בנתיב של טוקן המנהל, עם האימייל של המנהל כ-actor. note חובה, 1 עד 300 תווים (אחרי הסרת רווחים). מעבר למצב שהעסק כבר נמצא בו לא משנה דבר ולא נרשם (changed: false). קודים: 400, 401, 403, 404, 429
GET /v1/platform/businesses/:id/mode-changes—{ changes: [{ id, realDocuments, actor, note, at }] } – יומן המעברים של העסק, מהישן לחדש
PUT /v1/users/platform-admin (טוקן מנהל בלבד){ email, enabled, note? }{ userId, email, platformAdmin, changed, admins: [{ email, name }], changes } – הענקה או ביטול. 404 כשהמשתמש לא נמצא

איך מעניקים (המשתמש נרשם קודם באתר כרגיל). אין שום נתיב של משתמש שמעניק או מבטל – גם לא מנהל פלטפורמה אחר. רק טוקן המנהל (PUT /v1/users/platform-admin למעלה), או מפעיל עם גישה לבסיס הנתונים (למשל SQL Editor של Lovable Cloud, שבו טבלאות המנוע בסכימה invoice):

SQL
-- הענקה (נרשמת ביומן עם משתמש בסיס הנתונים שביצע אותה)
update invoice.users set platform_admin = true where email = 'owner@example.com';
-- או עם מי ולמה ביומן
select * from invoice.set_platform_admin('owner@example.com', true, '<מי מבצע>', '<למה>');
-- ביטול
select * from invoice.set_platform_admin('owner@example.com', false, '<מי מבצע>', '<למה>');
-- מי מנהל עכשיו, והיומן
select email, name from invoice.users where platform_admin;
select * from invoice.platform_admin_changes order by id;

כל שינוי של העמודה – דרך הפונקציה, דרך טוקן המנהל או ב-update ישיר – נרשם בטריגר ביומן platform_admin_changes (המשתמש, האימייל, המצב החדש, מי ביצע, הערה ומתי). ביומן אי אפשר לשנות או למחוק רשומה. הפונקציה רצה בהרשאות מי שקורא לה, וההרשאה ל-PUBLIC הוסרה ממנה. האימייל צריך להיות כמו שנרשם (באותיות קטנות).

ספר הלקוחות של היצרן (מנהל)

GET /v1/admin/customer-register מחזיר את כל ההרשמות ושינויי הפרטים (שם, כתובת, מספר עוסק, טלפון, אימייל, מתי), לפי נספח ה'(ה). הרשומות לא משתנות ולא נמחקות.

עיצובים ומותגים

מנוע 0.13: לעסק אחד יכולים להיות כמה עיצובים בשמות (מותגים) – למשל שני מותגים של אותו עוסק. העסק המשפטי אחד: שם העסק, מספר העוסק, הכתובת וכל מה שהוראות ניהול הפנקסים מחייבות מודפסים מפרטי העסק, זהים בכל עיצוב, וכל סוג מסמך ממוספר בסדרה אחת משותפת לכל העיצובים. העיצוב משנה רק מראה: לוגו, צבע הדגשה, תבנית (classic, modern, minimal) ושורת תחתית.

  • לכל עסק יש תמיד בדיוק עיצוב ברירת מחדל אחד (נאכף גם בבסיס הנתונים). עסק חדש מקבל עיצוב ראשון בשם העסק.
  • כל שינוי בעיצוב נשמר כגרסה חדשה שלא משתנה. כל מסמך שומר את העיצוב ואת הגרסה שהיו בתוקף כשהופק, ולכן המקור, כל העתק וכל תצוגה שלו נראים תמיד כמו ביום ההפקה – גם אחרי שהעיצוב השתנה או הועבר לארכיון.
  • עיצוב לא נמחק (מסמכים מפנים אליו). אפשר להעביר אותו לארכיון: אז אי אפשר להפיק בו מסמכים חדשים או לשנות אותו. את עיצוב ברירת המחדל אי אפשר להעביר לארכיון (קובעים קודם ברירת מחדל אחרת).
  • מזהה עיצוב: dsg_ ו-20 תווים הקסדצימליים. השם (1–60 תווים, ייחודי בעסק) פנימי ולא מודפס.
קריאהמיתוצאה
GET /v1/businesses/:id/designsכל מי שיש לו גישת צפייה (גם מפתח API){ designs: [...], defaultDesignId }. לכל עיצוב: { id, name, isDefault, archived, archivedAt, createdAt, version, versions, template, accentColor, footerText, hasLogo, logoType, logoBytes } – בלי הלוגו עצמו. ברירת המחדל ראשונה, אחריה הפעילים ובסוף שבארכיון. version: null = עוד לא נשמרה גרסה (העיצוב הבסיסי של המנוע)
GET /v1/designs/:designIdצפייההעיצוב המלא: { id, businessId, name, isDefault, archived, archivedAt, createdAt, version, template, accentColor, footerText, logo }. logo הוא data URL או null
POST /v1/businesses/:id/designsבעל העסק או מנהל{ name, template, accentColor, footerText?, logo?, isDefault? } → 201 עם שורת העיצוב (כמו ברשימה). isDefault: true קובע אותו גם כברירת המחדל
PUT /v1/designs/:designIdבעל העסק או מנהלגרסה חדשה: { template?, accentColor?, footerText?, logo?, name? }. שדה שלא נשלח נשאר כמו בגרסה הנוכחית (logo: null מסיר). name משנה שם. מחזיר את שורת העיצוב
POST /v1/designs/:designId/defaultבעל העסק או מנהלקובע את ברירת המחדל (לא עיצוב שבארכיון). מחזיר את שורת העיצוב
POST /v1/designs/:designId/archiveבעל העסק או מנהלהעברה לארכיון (לא את ברירת המחדל). קריאה חוזרת לא משנה דבר. מחזיר את שורת העיצוב
POST /v1/designs/:designId/previewצפייהPDF לדוגמה בעיצוב, בלי לשמור. גוף ריק = העיצוב כפי שהוא; עם שדות כמו ב-PUT = איך ייראה אחרי השינוי

logo: data URL (או base64) של PNG או JPEG, עד 300KB. accentColor בפורמט #RRGGBB; footerText עד 300 תווים. מפתח API וטוקן שירות לא יוצרים ולא משנים עיצובים (403), אבל בוחרים עיצוב בהפקה (designId) – זו לא הגדרה של העסק. שגיאות אימות (שם תפוס או ארוך, לוגו לא תקין, עיצוב בארכיון, ארכיון של ברירת המחדל) הן 422 עם הודעה בעברית; מזהה לא מוכר בנתיב הוא 404.

הפקה בעיצוב: designId אופציונלי ב-POST /v1/businesses/:id/documents (גם עם dryRun). בלעדיו – עיצוב ברירת המחדל. עיצוב לא מוכר, של עסק אחר או בארכיון: 422, ושום מספר לא מנוצל. התשובה, GET /v1/documents/:docId ותוצאת dryRun כוללות designId ו-designName; ברשימת המסמכים design_id ו-design_name. מסמך שהופק לפני 0.13 מוצג עם העיצוב הראשון של העסק (שבו הופק). ב-webhooks נוסף designId לאובייקט document (בלי השם).

דוגמה – רשימת העיצובים, ואז הפקה במותג השני:

Shell
curl -s https://<engine>/v1/businesses/$BIZ/designs -H "Authorization: Bearer $TOKEN"
JSON
{
  "designs": [
    { "id": "dsg_3f9c0a7e1b2d4c5e6f70", "name": "חוויה בקליק", "isDefault": true, "archived": false, "archivedAt": null,
      "createdAt": "2026-10-01T08:00:00.000Z", "version": 12, "versions": 3, "template": "classic", "accentColor": "#1a4a8c",
      "footerText": "", "hasLogo": true, "logoType": "png", "logoBytes": 18234 },
    { "id": "dsg_8a1b2c3d4e5f60718293", "name": "מדיה בקליק", "isDefault": false, "archived": false, "archivedAt": null,
      "createdAt": "2026-10-20T08:00:00.000Z", "version": 15, "versions": 1, "template": "modern", "accentColor": "#6a1b9a",
      "footerText": "תודה!", "hasLogo": false, "logoType": null, "logoBytes": 0 }
  ],
  "defaultDesignId": "dsg_3f9c0a7e1b2d4c5e6f70"
}
Shell
curl -s -X POST https://<engine>/v1/businesses/$BIZ/documents -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "docType": 305, "designId": "dsg_8a1b2c3d4e5f60718293",
        "customer": { "name": "עיריית רמלה", "vatNumber": "513746776" },
        "lines": [{ "description": "יום הפעלות", "quantity": 1, "unitPrice": 300000 }] }'
JSON
{ "id": 431, "docType": 305, "docNumber": 60012, "designId": "dsg_8a1b2c3d4e5f60718293", "designName": "מדיה בקליק", "...": "..." }

המסמך הבא, בכל עיצוב, יקבל את המספר 60013: הסדרה משותפת.

הנתיבים מלפני 0.13 (עיצוב ברירת המחדל)

ממשיכים לעבוד, על עיצוב ברירת המחדל של העסק:

קריאהתוצאה
GET /v1/businesses/:id/design{ version, template, accentColor, footerText, logo, designId, name }. logo הוא data URL או null; designId ו-name (מ-0.13) הם של עיצוב ברירת המחדל
PUT /v1/businesses/:id/design (בעל העסק){ template: "classic"|"modern"|"minimal", accentColor: "#RRGGBB", footerText, logo? } → { version, designId }. logo: data URL של PNG/JPEG עד 300KB, null להסרה, או בלי השדה כדי להשאיר את הקיים. כל שמירה היא גרסה חדשה של עיצוב ברירת המחדל. כאן שדה חסר מקבל ערך בסיסי (לא נשאר כמו קודם)
POST /v1/businesses/:id/design/previewאותו גוף, מחזיר PDF לדוגמה בלי לשמור (לוגו שלא נשלח – מעיצוב ברירת המחדל)

כל מסמך נשמר עם גרסת העיצוב שהייתה בתוקף כשהופק, ולכן העתק שמופק אחר כך נראה כמו המקור. העיצוב משנה רק מראה; כל השדות שהחוק מחייב מודפסים בכל תבנית.

מפתחות API (בעל העסק או מנהל)

קריאהתוצאה
POST /v1/businesses/:id/api-keys עם { name }201 { key, id, prefix, ... }. את key שומרים מיד; הוא לא יוצג שוב. משתמש: רק בכניסה עם סיסמה, לא בטוקן גישה אישי (403), כי המפתח לא פג ולא מתבטל עם הטוקן
GET /v1/businesses/:id/api-keysרשימת המפתחות (שם, תחילית, מתי נוצר ומתי בוטל), בלי המפתחות עצמם
DELETE /v1/api-keys/:keyIdביטול המפתח. הוא מפסיק לעבוד מיד, והרשומה נשארת

מסמכים

הפקה

POST /v1/businesses/:id/documents

JSON
{
  "docType": 320,
  "operator": "tami",
  "customer": { "key": "crm-1234", "name": "משפחת כהן", "vatNumber": "", "city": "ירושלים", "email": "..." },
  "lines": [{ "description": "הפעלה בקליקרים", "quantity": 1, "unitPrice": 90000 }],
  "pricesIncludeVat": true,
  "payments": [{ "method": 4, "amount": 90000, "paymentDate": "2026-09-22" }],
  "notes": "תודה!"
}
שדהערכים
docTypeראו טבלת סוגי המסמכים בהמשך
method1 מזומן, 2 שיק (חובה פרטי שיק ו-checkCrossedNotNegotiable), 3 אשראי, 4 העברה, 8 הוראת קבע, 9 אחר (methodLabel, למשל "ביט")
base{ "docType": 305, "docNumber": 60012 }

סוגי המסמכים (הקודים לפי נספח 1 למבנה האחיד, חוץ מהצעת מחיר):

קודמסמךמי רשאיהערות
10הצעת מחירכולםלא מסמך חשבונאי: סדרה משלה, לא נכלל במבנה האחיד, מקור ממוחשב בלי הודעה לפקיד השומה. validUntil = בתוקף עד
100הזמנהמורשה, חברה, פטור
200תעודת משלוחמורשה, חברה, פטוראפשר בלי מחירים (unitPrice: 0). base = הזמנה
210תעודת החזרהמורשה, חברה, פטוראפשר בלי מחירים. base = תעודת משלוח
300חשבון עסקה / חשבונית עסקהמורשה, חברה, פטוראצל מורשה: דרישת תשלום עם מע"מ, "אינו חשבונית מס", לא נספר כהכנסה. אצל פטור: החשבונית, בלי מע"מ
305חשבונית מסמורשה, חברהbase = הזמנה, תעודת משלוח או חשבון עסקה
320חשבונית מס / קבלהמורשה, חברהרק בתשלום מלא
330חשבונית מס זיכוימורשה, חברהחובה base (305/320) ו-creditReason
400קבלהכולםחובה notes כמהות התקבול, או base (305/300)
405קבלה על תרומהעמותהאישור סעיף 46 מפרטי העסק מודפס כשהוא בתוקף. דיווח ל"תרומות ישראל" עוד לא מחובר

סוג שנוסף אחרי שהעסק נרשם צריך סדרה: POST /v1/businesses/:id/sequences (בעל העסק), פעם אחת. עד המסמך הראשון מהסוג הזה אפשר להעלות את המספר הבא ב-PUT /v1/businesses/:id/sequences/:docType.

designId (אופציונלי, מנוע 0.13): העיצוב שבו המסמך יופק, מ-GET /v1/businesses/:id/designs; בלעדיו – עיצוב ברירת המחדל. ראו עיצובים ומותגים.

התשובה היא 201 עם { id, docNumber, amountNet, vatAmount, amountTotal, requiresManualSignature, designId, designName, allocation: { required, reason }, ... }.

בדיקה בלי הפקה (dryRun)

אותה קריאה עם "dryRun": true (ובאותה הרשאה, הפקה) בודקת ומחשבת את המסמך בדיוק כמו הפקה אמיתית: אותה פונקציה, אותן בדיקות, אותם סכומים ומע"מ, המספר הבא בסדרה וחובת מספר ההקצאה. המנוע אף כותב את המסמך בתוך טרנזקציה, ובסוף מבטל אותה. שום דבר לא נשמר: אין מסמך, אין אירוע ביומן, המונה של הסדרה לא מתקדם ושרשרת ה-hash לא משתנה.

JSON
{ "docType": 320, "customer": { "name": "משפחת כהן" }, "lines": [...], "payments": [...], "dryRun": true }

התשובה היא 200 עם המסמך כפי שהיה מופק (אותם שדות כמו GET /v1/documents/:docId: עסק, לקוח, שורות, תשלומים, סכומים), ועוד allocation, "dryRun": true ו-note. id ו-contentHash הם null, כי שום דבר לא נשמר. docNumber זמני: זה המספר שהמסמך יקבל אם יופק עכשיו, אלא אם יופק לפניו מסמך אחר באותה סדרה. בקשה לא תקינה נדחית בדיוק כמו בהפקה (422 עם אותה הודעה). dryRun חייב להיות true או false; ערך אחר (למשל "true" כמחרוזת) נדחה ב-400, כדי שלא יופק בטעות מסמך אמיתי.

המזהה הפנימי (id) של המסמך הבא אחרי בדיקה עשוי לדלג, כי מונה המזהים של Postgres לא חוזר אחורה בביטול טרנזקציה. זה לא מספר המסמך: docNumber נשאר רציף בלי פערים.

חשבונית זיכוי

JSON
{
  "docType": 330,
  "customer": { "key": "crm-1234", "name": "עיריית רמלה", "vatNumber": "513746776" },
  "lines": [{ "description": "ביטול חלק מההפעלה", "quantity": 1, "unitPrice": 100000 }],
  "base": { "docType": 305, "docNumber": 60012 },
  "creditReason": "ההפעלה קוצרה בשעה"
}

הזיכוי חייב להיות לאותו לקוח של החשבונית, ועד היתרה שלה. כמה כבר זוכה רואים ב-creditedNet של החשבונית (GET /v1/documents/:docId) או ב-credited_net ברשימת המסמכים. אין מחיקה ואין ביטול: כל תיקון הוא מסמך חדש.

קבלה על חשבונית

JSON
{
  "docType": 400,
  "customer": { "key": "crm-1234", "name": "עיריית רמלה", "vatNumber": "513746776" },
  "payments": [{ "method": 4, "amount": 354000, "paymentDate": "2026-10-15" }],
  "base": { "docType": 305, "docNumber": 60012 }
}
  • כש-allocation.required אמת, צריך לבקש מספר הקצאה מרשות המסים ולרשום אותו לפני הפקת המקור.
  • כש-requiresManualSignature אמת (למשל במזומן), אי אפשר להפיק מקור ממוחשב. מפיקים מקור להדפסה וחותמים עליו ביד.

שאר הקריאות

קריאהתיאור
GET /v1/businesses/:id/documents?from&to&docType&customerKey&limitרשימת מסמכים. לכל חשבונית גם credited_net (כמה זוכה) ומסמך הבסיס של זיכוי או קבלה; לכל מסמך design_id ו-design_name (העיצוב שבו הופק)
GET /v1/documents/:docIdכל פרטי המסמך. בחשבונית גם creditedNet; designId ו-designName – העיצוב שבו הופק
GET /v1/documents/:docId/eventsיומן הפעולות על המסמך: מתי הופק, מי רשם מספר הקצאה, מתי הופק המקור ומתי הודפסו העתקים
GET /v1/businesses/:id/events?after&limitיומן האירועים של כל העסק לפי הסדר (בהמשך). להודעות בזמן אמת: Webhooks
POST /v1/documents/:docId/allocation { allocationNumber, method: "manual"|"api" }רישום מספר הקצאה (9 עד 20 ספרות)
POST /v1/documents/:docId/original { computerized: true|false }ה-PDF של המקור. true = מקור ממוחשב חתום לשליחה ללקוח; false = מקור להדפסה. המקור מופק פעם אחת, וקריאה חוזרת מחזירה את אותו קובץ (X-Already-Produced: true)
GET /v1/documents/:docId/copyPDF של העתק

יומן האירועים של העסק

GET /v1/businesses/:id/events?after=<מזהה>&limit=<1..500> (צפייה) מחזיר את כל האירועים של מסמכי העסק – הפקה, מספר הקצאה, מקור, העתקים – לפי סדר המזהה, מהישן לחדש. מיועד לחיבור חיצוני שבודק מה חדש מאז הפעם הקודמת:

JSON
{
  "events": [
    { "id": 812, "documentId": 431, "docType": 305, "docNumber": 60012, "event": "issued",
      "at": "2026-10-15T09:12:44.000Z", "actor": "nissim@example.com (token Claude)", "details": { "contentHash": "..." } }
  ],
  "next": 812
}
  • after (ברירת מחדל 0): מחזיר רק אירועים שהמזהה שלהם גדול ממנו. limit ברירת מחדל 100, עד 500. ערך לא תקין: 400.
  • next הוא המזהה של האירוע האחרון בעמוד, או after עצמו כשאין חדשים. שולחים אותו כ-after בקריאה הבאה.
  • id, documentId, docType ו-docNumber הם תמיד מספרים (JSON number). at בפורמט ISO 8601 ב-UTC.
  • רק מסמכים של העסק שבנתיב. מפתח API רואה רק את העסק שלו, ומשתמש רק עסק שהוא שייך אליו.
  • בכל עסק המזהים נקבעים לפי סדר השמירה: כתיבות ליומן של אותו עסק ממתינות זו לזו עד סוף הטרנזקציה. לכן כשאירוע מופיע, כל האירועים הקודמים של העסק כבר מופיעים, וחיבור שמתקדם לפי next לא מפספס אף אירוע, גם כשכמה פעולות רצות במקביל.
  • המזהים לא רציפים: יש ביניהם אירועים של עסקים אחרים ומזהים של בדיקות בלי הפקה (dryRun) שבוטלו.

Webhooks

בעל העסק יכול לחבר נקודות קצה (webhooks) שמקבלות הודעה על כל אירוע במסמכי העסק, במקום לבדוק כל הזמן את יומן האירועים. יש שני סוגים:

  • standard – כתובת https של המערכת שלכם (CRM, הנהלת חשבונות וכו'). כל הודעה נשלחת ב-POST וחתומה לפי Standard Webhooks, בסוד חתימה (whsec_...) שמוצג פעם אחת ביצירה.
  • claude_routine – routine של Claude Code (claude.ai/code/routines) עם טריגר API. המנוע עצמו מפעיל אותו, בכתובת הקבועה https://api.anthropic.com/v1/claude_code/routines/<trig_...>/fire, עם הטוקן של ה-routine (sk-ant-oat01-...). הטוקן נשמר מוצפן, לא מוצג שוב ולא יוצא מהמנוע (גם לא למשגר).

סוגי האירועים (אירוע אחד ביומן = הודעה אחת):

אירועמתיברירת מחדל
document.issuedהופק מסמךstandard, claude_routine
document.original_producedהופק המקור (ממוחשב חתום או להדפסה)standard
document.copy_producedהופק העתק (כל הורדה של העתק)–
document.allocation_recordedנרשם מספר הקצאהstandard
webhook.testהודעת בדיקה (POST /v1/webhooks/:id/test)תמיד

קריאה ל-/copy יוצרת document.copy_produced. כדי להוריד PDF בתגובה ל-webhook השתמשו ב-/original (אחרי הפעם הראשונה בלי אירוע) או בנתוני ה-JSON, אחרת כל הורדה תיצור הודעה חדשה. לכן document.copy_produced לא נבחר כברירת מחדל, routine של Claude לא יכול לקבל אותו, ולאותו webhook ואותו מסמך נשלחת לכל היותר הודעת העתק אחת בכל 10 דקות.

הרשאות

רק בעל העסק (וטוקן המנהל, בחלק מהפעולות). עובד, רואה חשבון, מפתח API וטוקן שירות מקבלים 403.

  • T – בעל העסק בכניסה עם סיסמה (ies_) או בטוקן גישה אישי (iep_), או טוקן המנהל.
  • O – בעל העסק בכניסה עם סיסמה בלבד.
  • S – O, ועוד אימות מחדש בגוף הבקשה: currentPassword, ו-code כשיש אימות דו-שלבי (כמו ביצירת טוקן גישה). סיסמה שגויה: 401 (ונספרת לחסימה); בלי קוד: 401 עם twoFactorRequired: true. קלט שגוי נבדק קודם ומחזיר 422 בלי להיספר.

webhook ממשיך לשלוח נתונים גם אחרי שטוקן בוטל או שסיסמה הוחלפה, ולכן יצירה, הפעלה מחדש, הוספת אירועים, שינוי כתובת או טוקן, החלפת סוד והצגת הכתובת המלאה הם S. בעל העסק שביצע פעולת S אחרונה הוא האחראי ל-webhook; כשהוא מוסר מהעסק או מפסיק להיות בעל העסק, ה-webhook מושבת מיד (creator_removed), ובעל עסק אחר צריך לבדוק אותו ולהפעיל מחדש.

ההרשאה נבדקת שוב בתוך הטרנזקציה של השינוי: משתמש שהוסר מהעסק (או שכבר אינו בעל העסק) בזמן שהבקשה שלו רצה מקבל 403, גם אם הבדיקה בתחילת הבקשה עברה. PATCH מחליט אם הוא S לפי מצב ה-webhook בתחילת הבקשה; אם בינתיים ה-webhook הושבת או צומצם, כך שהשינוי הפך להפעלה מחדש או להוספת אירוע, והבקשה לא כללה כניסה עם סיסמה ואימות מחדש, התשובה היא 409 (ה-webhook השתנה בינתיים...) ושום דבר לא משתנה. שולחים שוב, והפעם נדרש S.

קריאההרשאהגוףתוצאה
GET /v1/businesses/:id/webhooksT—{ deliveryEnabled, limits, eventTypes: [{ type, name }], endpoints, audit }. deliveryEnabled: false = אין בשרת הזה משגר, ושום הודעה לא נשלחת
POST /v1/businesses/:id/webhooksS{ kind?: "standard", url, description?, eventTypes?, currentPassword, code? } או { kind: "claude_routine", routineId, routineToken, description?, eventTypes?, currentPassword, code? }201 נקודת הקצה, ועם secret (רק standard, פעם אחת). 422 כשאין משגר בשרת: שליחת webhooks לא מופעלת בשרת הזה
GET /v1/webhooks/:endpointIdT—נקודת הקצה
POST /v1/webhooks/:endpointId/reveal-urlS{ currentPassword, code? }{ url } – הכתובת המלאה, עם הפרמטרים. נרשם בהיסטוריית השינויים
PATCH /v1/webhooks/:endpointIdT; S להפעלה, להוספת אירוע, לכתובת ולטוקן{ description?, eventTypes?, enabled?, url?, routineToken?, currentPassword?, code? }נקודת הקצה. גוף ריק או שדה לא מוכר: 400. ה-webhook השתנה בינתיים והשינוי דורש עכשיו S: 409
DELETE /v1/webhooks/:endpointIdT—{ ok: true }. מחיקה סופית, עם המשלוחים; רישום המחיקה נשאר בהיסטוריה
POST /v1/webhooks/:endpointId/rotate-secretS{ previousSecretValidHours?: 0..72, currentPassword, code? }נקודת הקצה עם secret החדש (פעם אחת). רק standard
POST /v1/webhooks/:endpointId/testT (routine: O)—202 המשלוח של הודעת הבדיקה. עד 5 בשעה (429); webhook מושבת: 422
GET /v1/webhooks/:endpointId/deliveries?status&before&limitT—{ deliveries, next } מהחדש לישן. limit 1..200 (50); before = next מהעמוד הקודם; status = pending/succeeded/failed/cancelled
POST /v1/webhook-deliveries/:deliveryId/retryT (routine: O)—המשלוח, שוב בתור. רק failed או cancelled, צעיר מ-29 יום, ב-webhook פעיל; עד 50 בשעה לכל webhook (429)

התשובות של היצירה, reveal-url ו-rotate-secret נשלחות עם Cache-Control: no-store.

נקודת קצה (WebhookEndpointInfo):

JSON
{
  "id": "7d0e5b8a-3c2f-4e1d-9b6a-0f1e2d3c4b5a", "businessId": "5f0c3c1e-...", "kind": "standard",
  "url": "https://api.my-crm.com/webhooks/…?token=***", "urlHasQuery": true, "urlMasked": true, "routineId": null,
  "description": "CRM", "eventTypes": ["document.issued", "document.original_produced", "document.allocation_recorded"],
  "secretHint": "…Hh8=", "previousSecretExpiresAt": null,
  "enabled": true, "disabledReason": null, "disabledAt": null,
  "consecutiveFailures": 0, "failingSince": null, "lastSuccessAt": "2026-10-15T09:12:45.120Z", "lastFailureAt": null, "holdUntil": null,
  "createdAt": "...", "createdBy": "nissim@example.com", "updatedAt": "...", "stats": { "pending": 0, "failed24h": 0 }
}
  • הכתובת מוצגת תמיד מקוצרת, כי הנתיב והפרמטרים כוללים לפעמים סיסמה של המערכת המקבלת (Slack, Discord, Zapier ו-Make שמים אותה בנתיב): רק הקטע הראשון בנתיב, וגם הוא רק כשהוא מילה (עד 24 תווים, לכל היותר 2 ספרות), ואחריו …; בפרמטרים name=*** כשהשם מילה, ו-*** לכל חלק אחר. למשל https://hooks.slack.com/services/T0/B0/XXXX מוצג https://hooks.slack.com/services/…, ו-https://hook.eu1.make.com/<טוקן> מוצג https://hook.eu1.make.com/…. urlMasked: true כשחלק מהכתובת מוסתר. הכתובת המלאה רק ב-reveal-url. גם בהיסטוריית השינויים הכתובת מקוצרת כך. ב-routine של Claude הכתובת קבועה ונגזרת מהמזהה, ולכן מוצגת במלואה.
  • routineId ב-routine: אפשר להדביק את המזהה (trig_...) או את כתובת ה-API שמוצגת ב-claude.ai; המנוע לוקח ממנה את המזהה.
  • עד 10 webhooks לעסק (כולל מושבתים), וכתובת אחת פעם אחת בלבד (routine – מזהה אחד פעם אחת).
  • description עד 200 תווים, בלי תווי בקרה.

כללי הכתובת (standard)

https:// בלבד, בפורט הרגיל (443), לשם מתחם ולא לכתובת IP (בכל צורה), בלי שם משתמש וסיסמה, בלי #, עד 2,048 תווים. אסורים localhost ושמות פנימיים או שמורים: .local, .localdomain, .internal, .intranet, .lan, .home, .corp, .private, .arpa (כולל home.arpa), .test, .example, .invalid, .onion, ושם בלי נקודה. פרמטרים (?a=b) מותרים. הכתובת נשמרת מנורמלת (אותיות קטנות בשם המתחם, punycode, בלי :443). שם מתחם ציבורי שמפנה לכתובת פנימית (למשל 10.0.0.1.nip.io) נעצר ברמת הרשת, אצל המשגר: שרת ה-Node לא מתחבר לכתובות פרטיות, שמורות, loopback, link-local, CGNAT, multicast ו-IPv4 בתוך IPv6 (ועוד טווחים ב-ENGINE_WEBHOOK_BLOCK_CIDRS), ו-Cloudflare Workers לא מתחבר לרשתות פרטיות. האתר גם לא שולח לכתובות בדומיין שלו עצמו (למשל *.lovable.app); מקבל שמתארח שם משתמש בכתובת אחרת.

גוף ההודעה

JSON בלי רווחים, שנבנה פעם אחת כשהאירוע נכתב ונשלח בדיוק כך בכל ניסיון. רק מזהים, סוג, מספר, תאריך וסכומים (באגורות): בלי פרטי לקוח, בלי מי שביצע ובלי שם העסק. את שאר הפרטים קוראים ב-API, בהרשאות של המקבל (למשל GET /v1/documents/:docId עם data.document.id).

{
  "type":      "document.issued" | "document.original_produced" | "document.copy_produced" | "document.allocation_recorded" | "webhook.test",
  "timestamp": זמן האירוע (ISO 8601 UTC),
  "data": {
    "eventId":    המזהה ביומן האירועים (בלי ב-webhook.test),
    "businessId": "...",
    "document": { "id", "docType", "docTypeName", "docNumber", "documentDate", "currency", "amountNet", "vatAmount", "amountTotal",
                  "designId"?: (מנוע 0.13: מזהה העיצוב שבו הופק, בלי השם),
                  "baseDocument"?: { "docType", "docNumber" }, "allocationNumber"?: (רק ב-document.allocation_recorded) },
    "original"?: { "kind": "computerized" | "printed" },   (רק ב-document.original_produced)
    "endpointId"?, "message"?                                (רק ב-webhook.test)
  }
}
JSON
{"type":"document.issued","timestamp":"2026-10-15T09:12:44.000Z","data":{"eventId":812,"businessId":"5f0c3c1e-8a4b-4c47-9a39-2f7d2b6f1a10","document":{"id":431,"docType":305,"docTypeName":"חשבונית מס","docNumber":60012,"documentDate":"2026-10-15","currency":"ILS","amountNet":300000,"vatAmount":54000,"amountTotal":354000}}}
JSON
{"type":"document.original_produced","timestamp":"2026-10-15T09:13:02.517Z","data":{"eventId":813,"businessId":"5f0c3c1e-8a4b-4c47-9a39-2f7d2b6f1a10","document":{"id":431,"docType":305,"docTypeName":"חשבונית מס","docNumber":60012,"documentDate":"2026-10-15","currency":"ILS","amountNet":300000,"vatAmount":54000,"amountTotal":354000},"original":{"kind":"computerized"}}}
JSON
{"type":"document.copy_produced","timestamp":"2026-10-16T07:40:11.204Z","data":{"eventId":820,"businessId":"5f0c3c1e-8a4b-4c47-9a39-2f7d2b6f1a10","document":{"id":433,"docType":400,"docTypeName":"קבלה","docNumber":70031,"documentDate":"2026-10-16","currency":"ILS","amountNet":354000,"vatAmount":0,"amountTotal":354000,"baseDocument":{"docType":305,"docNumber":60012}}}}
JSON
{"type":"document.allocation_recorded","timestamp":"2026-10-15T09:20:00.000Z","data":{"eventId":815,"businessId":"5f0c3c1e-8a4b-4c47-9a39-2f7d2b6f1a10","document":{"id":431,"docType":305,"docTypeName":"חשבונית מס","docNumber":60012,"documentDate":"2026-10-15","currency":"ILS","amountNet":300000,"vatAmount":54000,"amountTotal":354000,"allocationNumber":"202610150001234"}}}
JSON
{"type":"webhook.test","timestamp":"2026-10-15T10:00:00.000Z","data":{"businessId":"5f0c3c1e-8a4b-4c47-9a39-2f7d2b6f1a10","endpointId":"7d0e5b8a-3c2f-4e1d-9b6a-0f1e2d3c4b5a","message":"הודעת בדיקה. אין מסמך חדש."}}

חתימה (standard)

כל בקשה נשלחת ב-POST עם הכותרות:

כותרתערך
content-typeapplication/json
user-agentInvoiceEngine-Webhooks/<גרסה>, למשל InvoiceEngine-Webhooks/0.10.0
webhook-idמזהה ההודעה (msg_ ו-32 תווי hex). קבוע בכל הניסיונות של אותה הודעה: לפיו מזהים כפילויות
webhook-timestampשניות מאז 1970 (UTC), של הניסיון הזה
webhook-signaturev1,<base64>; בזמן החלפת סוד שתי חתימות מופרדות ברווח: v1,<חדש> v1,<קודם>

signature = base64(HMAC-SHA256(key = base64decode(הסוד בלי "whsec_"), message = "{webhook-id}.{webhook-timestamp}.{body}")), כש-body הוא הבתים של גוף הבקשה כמו שהתקבלו (לא JSON שפוענח ונבנה מחדש). דחו הודעה כש-webhook-timestamp רחוק מעכשיו ביותר מ-5 דקות. וקטור לבדיקה (הסוד הוא הבתים 0x00..0x1f):

secret    = whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
webhook-id = msg_0123456789abcdef0123456789abcdef
webhook-timestamp = 1792055564
body      = הדוגמה של document.issued למעלה, בדיוק (333 בתים ב-UTF-8)
signature = v1,Vj1weAfat3Gnkclb/AS+L5gH7UB52TP/WeXEsvRcSuw=

בדיקה ב-Node.js (בלי תלויות):

JavaScript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(secret, headers, rawBody, nowSeconds = Date.now() / 1000) {
  const id = headers["webhook-id"], timestamp = headers["webhook-timestamp"], signatures = headers["webhook-signature"] ?? "";
  if (!id || !/^\d+$/.test(timestamp ?? "") || Math.abs(nowSeconds - Number(timestamp)) > 300) throw new Error("bad timestamp");
  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
  const valid = signatures.split(" ").some((entry) => {
    const [version, value] = entry.split(",");
    const given = Buffer.from(value ?? "", "base64");
    return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
  });
  if (!valid) throw new Error("bad signature");
  return JSON.parse(rawBody);
}

בדיקה ב-Python (בלי תלויות; raw_body הם הבתים של הגוף, headers עם שמות באותיות קטנות או אובייקט כותרות שלא מבחין בין אותיות):

Python
import base64, hashlib, hmac, json, time

def verify_webhook(secret, headers, raw_body, now=None):
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp") or ""
    signatures = headers.get("webhook-signature") or ""
    now = time.time() if now is None else now
    if not msg_id or not timestamp.isdigit() or abs(now - int(timestamp)) > 300:
        raise ValueError("bad timestamp")
    key = base64.b64decode(secret[len("whsec_"):])
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    for entry in signatures.split(" "):
        version, _, value = entry.partition(",")
        if version == "v1" and hmac.compare_digest(value, expected):
            return json.loads(raw_body)
    raise ValueError("bad signature")

אפשר גם להשתמש בספריות הרשמיות של Standard Webhooks (standardwebhooks, בכל שפה).

החלפת סוד (rotate-secret): הסוד החדש מוחזר פעם אחת. בלי previousSecretValidHours (ברירת מחדל 24) הסוד הקודם ממשיך לחתום במשך 24 שעות, וכל הודעה נושאת שתי חתימות, כדי שתספיקו לעדכן את המערכת המקבלת. כשהסוד דלף שולחים previousSecretValidHours: 0, והסוד הקודם מפסיק מיד. הסוד נשמר במנוע רק מוצפן (AES-256-GCM, במפתח שנגזר מסיסמת מפתחות החתימה של השרת).

מסירה

  • לפחות פעם אחת (standard): אותה הודעה יכולה להגיע יותר מפעם אחת (למשל כשהתשובה שלכם אבדה בדרך). זהו כפילויות לפי webhook-id. אין הבטחה לסדר: השתמשו ב-data.eventId (עולה לפי סדר השמירה בעסק), או קראו את המצב ב-API.
  • יומן האירועים הוא המקור המוסמך (GET /v1/businesses/:id/events): ההודעות נכתבות לתור באותה טרנזקציה של האירוע, אבל תקלה בתור לעולם לא מכשילה הפקה של מסמך (ואז ההודעה לא נוצרת). להתאמה מלאה, השוו מדי פעם מול היומן.
  • זמן: בדרך כלל תוך שניות; לכל היותר כחצי דקה אחרי האירוע (בלי pg_cron 1.5 ומעלה: עד דקה), ויותר כשה-webhook בהשהיה של המפסק (בהמשך).
  • תשובה: כל 2xx היא הצלחה. הגוף לא נקרא. הפניות (3xx) לא נעקבות ונחשבות כישלון. זמן קצוב: 10 שניות.

ניסיונות חוזרים (standard, עד 8 ניסיונות, כ-44 שעות). לכל השהיה תזוזה קבועה של ±10% לכל הודעה:

אחרי ניסיון שנכשל12345678
הניסיון הבא בעודדקה5 דקות30 דקותשעתיים6 שעות12 שעות24 שעות— נכשל
  • 429 ו-503: לפי Retry-After (שניות או תאריך), לא פחות מההשהיה שבטבלה ולא יותר מ-24 שעות.
  • 410: הכתובת לא קיימת עוד – ה-webhook מושבת (gone).
  • מפסק: אחרי 3 כישלונות ברצף לא שולחים ל-webhook במשך 1, 5, 15, 30 דקות, ואחר כך שעה אחרי כל כישלון נוסף, ונשלחת רק הודעה אחת בכל פעם (בדיקה). הצלחה משחררת את כל מה שממתין (עד 5 בכל סבב).
  • השבתה אוטומטית: אחרי 10 כישלונות ברצף שנמשכים 72 שעות לפחות (failing); 410 (gone); כתובת שמפנה לרשת פנימית (blocked_address) או שלא עוברת את בדיקת הכתובות של המשגר (invalid_url); סוד שאי אפשר לפענח (secret_unreadable); 1,000 הודעות שממתינות (backlog); האחראי כבר לא בעל העסק (creator_removed). כשהתוצאה מגיעה ממשגר חיצוני (Cloudflare Worker), 410 / כתובת חסומה / כתובת לא תקינה הן "פסילה": ההודעה נשלחת שוב, וה-webhook מושבת רק אחרי 3 פסילות במשך 10 דקות לפחות. הצלחה מאפסת אותן.
  • השהיה מול השבתה: PATCH { enabled: false } היא השהיה (manual): הודעות שכבר המתינו בזמן ההשהיה נשמרות ונשלחות אחרי הפעלה מחדש (S). אירועים שקורים בזמן ההשהיה לא נכנסים לתור ולא יישלחו גם אחרי ההפעלה; מי שצריך אותם קורא את יומן האירועים (GET /v1/businesses/:id/events). בהשבתה אוטומטית ההודעות שממתינות מבוטלות (cancelled), והפעלה מחדש לא שולחת אותן; אפשר לשלוח שוב משלוח שבוטל או נכשל, אחד אחד (retry), עם אותו webhook-id ואותו גוף.
  • שמירה: משלוחים שהסתיימו נמחקים 30 יום אחרי שנוצרו, ממתינים (למשל ב-webhook מושהה) אחרי 31 יום; היסטוריית השינויים אחרי שנתיים.

משלוח (WebhookDeliveryInfo): { id, endpointId, messageId, eventType, eventId, documentId, status, attempts, nextAttemptAt, lastAttemptAt, lastStatusCode, lastError, lastDurationMs, resultUrl, createdAt, completedAt, payload }. nextAttemptAt: null בממתין = מיד. lastError הוא הסבר קצר בעברית (בלי גוף התשובה, בלי כתובות ובלי טוקנים).

routine של Claude Code

  • יוצרים routine ב-claude.ai/code/routines, מחברים אליו את המחבר של מערכת הפקת המסמכים, ובחלק Select a trigger בוחרים API ולוחצים Generate token. את הכתובת (או המזהה trig_...) ואת הטוקן (sk-ant-oat01-...) מדביקים ביצירה (S). הטוקן מוחלף ב-PATCH { routineToken } (S).
  • מומלץ לשלוח רק document.issued (ברירת המחדל), ו-routine ייעודי שמחובר רק למחבר של מערכת הפקת המסמכים. פרטי הלקוחות שה-routine קורא מהמסמכים נשמרים בהיסטוריית השיחות ב-claude.ai.
  • המנוע שולח בעצמו: POST לכתובת הקבועה עם authorization: Bearer <הטוקן>, anthropic-version: 2023-06-01 וגוף {"text": "..."}. הטקסט: שורה בעברית לכל אירוע, ואחריה מערך ה-JSON של ההודעות (בדיוק הגוף של ה-webhooks).
  • קבוצות ומרווח: הפעלה אחת לכל האירועים שממתינים (עד 20), ולכל היותר הפעלה אחת בכל 3 דקות לכל routine (עד 20 בשעה, מתוך 30 שמותרות ל-routine).
  • בלי ניסיון חוזר אחרי תוצאה לא ודאית: ל-API של Claude אין מזהה שמונע כפילות, וכל הפעלה פותחת שיחה. המנוע מנסה שוב לבד רק כשבטוח שלא נפתחה שיחה: 429 (לפי Retry-After, בין דקה לשעה, עד 24 שעות מהאירוע, בלי להיחשב ככישלון), 500/503/529 וחיבור שלא הוקם (עד 4 ניסיונות). זמן קצוב (30 שניות), תקלת רשת או 5xx אחר: נכשל, עם האזהרה "ייתכן שנפתחה שיחה ב-Claude". 400 (למשל routine מושהה) משבית (routine_paused_or_invalid); 401/403/404 משביתים (routine_rejected).
  • שליחה חוזרת ידנית ובדיקה של routine פותחות שיחה חדשה בחשבון של בעל העסק, ולכן רק בכניסה עם סיסמה (O), עם אזהרה באתר.
  • הצלחה: resultUrl של המשלוח הוא הקישור לשיחה (https://claude.ai/code/session_...).

Channels והתראות שרת של claude.ai לא נתמכים: הם עובדים רק בתוך Claude Code מקומי, ולא משרת מרוחק.

היסטוריית שינויים

כל יצירה, עדכון, החלפת סוד, הצגת כתובת, הפעלה, השהיה, השבתה אוטומטית ומחיקה נרשמים (audit ב-GET .../webhooks, 50 האחרונים): { id, endpointId, action, actor, kind, url, eventTypes, detail, at }, עם הכתובת המקוצרת ומי שביצע (system בהשבתה אוטומטית). הרשומות נשארות גם אחרי מחיקת ה-webhook, ונמחקות אחרי שנתיים.

המשגר (/v1/internal/webhooks/*)

מי ששולח בפועל את משלוחי ה-standard הוא משגר: בשרת ה-Node הרשום, לולאה בתוך התהליך (ENGINE_WEBHOOK_DISPATCHER=inprocess, עם ההגנה ברמת הרשת); בהדגמה על Lovable Cloud, ה-Worker של האתר (POST /api/webhooks/dispatch), ש-pg_net ו-pg_cron מעירים. משגר חיצוני מדבר עם המנוע בשלוש קריאות, בטוקן המשגר בלבד:

קריאהגוףתוצאה
POST /v1/internal/webhooks/claim{ limit?: 1..20 } (ברירת מחדל 10){ requests: [{ leaseToken, deliveryId, endpointId, url, headers, body, timeoutMs }], leaseSeconds: 60 } – בקשות חתומות ומוכנות לשליחה. עד 5 לכל webhook (אחת כשהוא נכשל ברצף), ובלי webhook שמשלוח שלו כבר בדרך. עד 120 קריאות בדקה (429)
POST /v1/internal/webhooks/report{ results: [{ leaseToken, deliveryId, outcome, status?, durationMs, retryAfter?, error? }] } (1..50){ accepted, ignored }
POST /v1/internal/webhooks/routines{}{ fired, succeeded, failed, retried, ms } – המנוע מפעיל בעצמו את ה-routines שהגיע זמנם
  • outcome: response (עם status 100..599), timeout, network, connect (החיבור לא הוקם), blocked (כתובת פנימית), invalid_url. המשגר מדווח רק עובדות; המנוע מחליט על ניסיון חוזר והשבתה.
  • חכירה: כל בקשה שנלקחה שמורה למשגר 60 שניות. דיווח מתקבל רק עם leaseToken הנוכחי של המשלוח, ולכן מותר לשלוח אותו שוב אחרי תקלת רשת (בפעם השנייה הוא ignored). בלי דיווח החכירה פגה והמשלוח יילקח שוב.
  • לפני כל שליחה המשגר בודק שוב את הכתובת (אותם כללים, והכתובת חייבת להיות כבר מנורמלת), שולח רק את הכותרות שבבקשה, לא עוקב אחרי הפניות ולא קורא את גוף התשובה.
  • בקשה לא כוללת אף פעם authorization או טוקן של routine.

אישור קצר-מועד whd1: במקום הטוקן עצמו, המשגר שולח Authorization: Bearer whd1.<unix>.<hex>, כש-unix הוא הזמן בשניות (10 ספרות) ו-hex הוא HMAC-SHA256 בהקסדצימלי (אותיות קטנות), במפתח = הבתים של טוקן המשגר (UTF-8), על invoice-engine/dispatch/v1|<unix>. תקף כשהזמן רחוק מעכשיו לכל היותר 120 שניות. וקטור:

token  = 0f1e2d3c4b5a69788796a5b4c3d2e1f00112233445566778899aabbccddeeff
unix   = 1792055564
result = whd1.1792055564.a7be6b62a5030574d45830e37a3fe699fb94bb8414275180233e5e369a059f12

ב-SQL (pgcrypto): 'whd1.' || unix || '.' || encode(hmac('invoice-engine/dispatch/v1|' || unix, token, 'sha256'), 'hex'). ה-Edge Function מקבל רק whd1. שרת Node מקבל גם את הטוקן עצמו רק עם ENGINE_WEBHOOK_DISPATCH_RAW=1. בלי טוקן משגר (ובלי משגר בתוך התהליך) הנתיבים האלה מחזירים 404, ויצירת webhook מחזירה 422.

הסכמה לקבלת מסמכים ממוחשבים (הוראה 18ב(ג))

  • POST /v1/businesses/:id/consents עם { customerKey, granted: true, channel: "quote-signature", evidence: { ip, at, ... } }
  • GET /v1/businesses/:id/consents/:customerKey → { granted }

מקור ממוחשב מופק רק ללקוח שההחלטה האחרונה שלו היא הסכמה.

מבנה אחיד

POST /v1/businesses/:id/openformat עם { from, to } מחזיר ZIP ובתוכו OPENFRMT/<8 ספרות>.<שנה>/<MMDDhhmm>/INI.TXT ו-BKMVDATA.zip. עם ?format=json מתקבל רק הסיכום: מספר רשומות לכל סוג ודוח המסמכים של נספח 1.

חיבור מה-CRM

את הקריאות שולחים מצד השרת בלבד (Supabase Edge Function), כשמפתח ה-API של העסק שמור בסודות. מפתח API אסור לשים בקוד הדפדפן. בדפדפן משתמשים רק בטוקן כניסה של משתמש.

התיעוד הזה הוא engine/docs/API.md של המנוע. אותו תוכן, עם הקדמה לסוכני AI ורשימת כלי ה-MCP, ב-/docs/api.md.