כל תשלום ב-Stripe Checkout, ב-Payment Link או בחשבונית Stripe הופך אוטומטית למסמך מס, וכל החזר הופך לחשבונית זיכוי.
מתאים לאתרים בלי חנות מובנית (Framer, Carrd, Softr, Bubble, אתרי Notion ועוד) שמוכרים דרך קישור תשלום של Stripe.
התקנה (5 דקות)#
- ב-Stripe Dashboard: Developers → Webhooks → Add endpoint (או Workbench → Event destinations).
- ב-Endpoint URL הדביקו את כתובת ה-Webhook שמוצגת אצלכם במסך החיבור (
/billing/ecommerce, כרטיס Stripe). - בחרו את האירועים:
checkout.session.completed,checkout.session.async_payment_succeeded,invoice.paid,charge.refunded. - אחרי השמירה לחצו Reveal על ה-Signing secret (מתחיל ב-
whsec_) והדביקו אותו בשדה הסוד. - אופציונלי, מומלץ: צרו Restricted key עם הרשאת קריאה ל-Checkout Sessions והדביקו אותו בשדה "מפתח API סודי". עם המפתח המסמך כולל את שורות המוצרים. בלי מפתח המסמך כולל שורה אחת בשם "תשלום" על הסכום ששולם.
- בחרו סוג מסמך ולחצו על "שליחת הזמנת בדיקה".
מה קורה בפועל:
- תשלום חד-פעמי (Checkout או Payment Link) מפיק מסמך.
- מנוי: החשבונית של המנוי (
invoice.paid) מפיקה מסמך לכל חיוב. ה-Session של מנוי לא מפיק בנפרד, כדי שלא יופקו שני מסמכים. - החזר מלא מפיק חשבונית זיכוי על כל המסמך. החזר חלקי מפיק זיכוי על הסכום שהוחזר.
Developer reference#
Endpoint: POST /api/billing/ecommerce/stripe/<hookId>
Auth: Stripe-Signature: t=<unix>,v1=<hex> = HMAC-SHA256(signing secret, "<t>.<rawBody>"), tolerance 5 minutes. Every v1 is tried (a rolled secret keeps two live for 24h); all other schemes are ignored. Docs: https://docs.stripe.com/webhooks#verify-manually
| Event | Result |
|---|---|
checkout.session.completed / async_payment_succeeded | document, only when payment_status = "paid", mode = "payment" and no invoice on the session |
invoice.paid | document (amount_paid > 0) |
charge.refunded | credit note (refund) |
| anything else | 200 {skipped} |
Money: Stripe amounts are integers in the currency's smallest unit; for ILS that is agorot, so nothing is multiplied. Amounts come from the gross figures Stripe reports (line amount_total, after discount and tax) with taxIncluded: true, so the payment row equals the document total exactly.
| Stripe field | Document |
|---|---|
customer_details.name / email / address | customer name (fallback לקוח/ה), email, address |
customer_details.tax_ids[type=il_vat].value | customerUniqueId |
amount_total | the single payment row (credit card; PayPal when payment_method_types has paypal and not card) |
line items description × quantity, amount_total | lines (pulled, see below); quantity moves into the name when the gross does not divide evenly |
amount_total − Σ line amount_total | extra line משלוח (or תוספת) |
total_details.amount_tax | document vatBp, derived as tax / (total − tax) and snapped to 18% / 17%; foreign or zero tax uses the statutory rate |
total_details.amount_discount | mentioned in the document comment (already inside the line amounts) |
currency | uppercased; a non-ILS currency needs an exchange rate, so the order is recorded as error in the orders log |
Pull mode: the session event has no line_items. With the secret_key credential the connector calls
GET https://api.stripe.com/v1/checkout/sessions/{id}/line_items?limit=100 with Authorization: Bearer <key>
(https://docs.stripe.com/api/checkout/sessions/line_items). No key, or Stripe answers 401 / 403 / 404: one line תשלום for the amount paid. Transport error, 429 or 5xx: the receiver answers 502 and Stripe retries.
Idempotency / refunds: externalId is the PaymentIntent id when present (else the session or invoice id), so charge.refunded (which only carries the PaymentIntent) finds the original document. Limits:
- Refund of a subscription invoice on recent API versions cannot be matched (an invoice has no
payment_intentand a charge has noinvoice): it is recorded aserrorin the orders log. Issue that credit note by hand. charge.refundsis not sent by default, so the refund id is<charge id>:<amount_refunded>; a partial refund credits the delta againstprevious_attributes.amount_refundedwhen Stripe sends it.
Test with curl (signed sample against your own hookId):
HOOK="https://<your-host>/api/billing/ecommerce/stripe/<hookId>"
WHSEC="whsec_..." # the secret you pasted in the connector
BODY='{"id":"evt_test","type":"checkout.session.completed","created":'$(date +%s)',"data":{"object":{"id":"cs_test_1","mode":"payment","payment_status":"paid","payment_intent":"pi_test_1","invoice":null,"currency":"ils","amount_subtotal":20000,"amount_total":23600,"total_details":{"amount_discount":0,"amount_shipping":0,"amount_tax":3600},"payment_method_types":["card"],"customer_details":{"name":"יעל מזרחי","email":"[email protected]","tax_ids":[]}}}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$WHSEC" -hex | sed 's/^.* //')
curl -i -X POST "$HOOK" -H "Content-Type: application/json" -H "Stripe-Signature: t=$T,v1=$SIG" -d "$BODY"Troubleshooting#
- 401 invalid signature: הסוד בשדה שונה מה-Signing secret של אותו Endpoint (לכל Endpoint יש סוד משלו, וגם מצב Test ומצב Live נבדלים). בדקו גם ששעון המכשיר שלכם מסונכרן, כי חתימה בת יותר מ-5 דקות נדחית.
- 401 secret is not configured: עדיין לא הדבקתם Signing secret בחיבור.
- 404 unknown hook: כתובת ה-Webhook הועתקה חלקית או שנוצרה מחדש. העתיקו אותה שוב מהמסך.
- המסמך הופק בשורה אחת "תשלום": לא הוגדר מפתח API, או שהמפתח חסר הרשאת קריאה ל-Checkout Sessions.
- הזמנה בסטטוס blocked או dormant: מגבלת החבילה או מודול החיוב כבוי. ההזמנה נשמרת ואפשר להפיק אותה ידנית מיומן ההזמנות.
- הזמנה בסטטוס error עם מטבע זר: מסמך במטבע שאינו שקל דורש שער המרה. הפיקו אותה ידנית עם שער.