SLK Invoices issues an Israeli tax document (חשבונית מס / קבלה / חשבונית מס קבלה / חשבונית זיכוי) for every paid order that reaches it. A connector is how a website builder, store platform, payment gateway or form tool reaches it. This page is the complete recipe — a working connector is one TypeScript file, one fixture test and one wiki page.
1. Pick the mode#
| Mode | When | What you write |
|---|---|---|
hook | The platform can POST a webhook to a URL | A ConnectorDef in src/ecommerce/connectors/defs/<id>.ts |
plugin | Our code runs inside the platform (WordPress, PrestaShop…) | A plugin under integrations/<platform>/ that calls the REST API |
recipe | A third tool does the wiring (Zapier / Make / n8n) | A ConnectorDef with steps + a wiki page |
Hook connectors get everything for free: the receiver route
POST /api/billing/ecommerce/<id>/<hookId>, hookId → tenant resolution, per-IP and per-hook
rate limits, a 512 KB body cap, signature verification, JSON/form parsing, idempotency by
(source, external_id), the plan gate and monthly quota, refunds → credit notes, the settings
screen (secret, credentials, document type, "send test order"), the orders log and the
developer-portal page.
2. Read these first#
src/ecommerce/connectors/types.ts— theConnectorDefcontract (every field is documented).src/ecommerce/connectors/inbound.ts— what the generic receiver does with your def.src/ecommerce/connectors/verify.ts— HMAC / timestamped / token helpers; never roll your own compare.src/ecommerce/connectors/defs/shopify.ts— a complete push-mode example.src/ecommerce/mapping.ts— money conventions used by the mappers.test/connectors-framework.test.ts— how a def is exercised end-to-end in-process.
3. Money and document conventions (non-negotiable)#
- Money is integer agorot (
unitPriceAgorot,amountAgorot), quantities are thousandths (quantityMilli: 2000= 2), VAT is basis points (vatBp: 1800= 18%). Never floats. taxIncludedstates whether line prices are gross. Derive per-linevatBpfrom the platform's own tax data when it exists (Shopifytax_lines[].rate, WooCommercetotal_tax/total); a line with no tax data inherits the documentvatBp(STANDARD_VAT_BP).- Shipping is its own line ("משלוח"), a document-level discount uses
discountValue+DISCOUNT_TYPE.fixed. For document type 3 the payments must total the document exactly — the engine refuses otherwise; use the platform's paid total as the single payment row. externalId= the platform's own order id (string).apiIdentifier="<id>-<externalId>"— the source prefix is enforced by the registry test.- Refund ids come from the platform (never
Date.now()): a redelivered refund must map to the same row.mapRefundreturnsamountAgorotfor a partial refund,nullfor a full one. - Customer name falls back to
"לקוח/ה"; email/address are optional. VAT id (ח.פ./ת.ז.) goes incustomerUniqueIdwhen the platform has it.
4. Authentication — pick the strongest the platform offers#
| Platform signs with | auth |
|---|---|
| HMAC-SHA256 of the raw body in a header (Shopify, Webflow, PayPlus…) | { kind: "hmac", header, encoding: "hex" | "base64", prefix?, message? } |
t=…,v1=… timestamped scheme (Stripe) | { kind: "custom", verify } using verifyTimestampedHmac |
| A shared token the merchant pastes into the URL / header (many gateways) | { kind: "token", header, query? } |
| Nothing (only the URL) | { kind: "hookid" } — the receiver adds a per-hook rate limit and strict order-id checks |
| Verification by calling the platform's API (PayPal) | { kind: "custom", verify } — inject fetch via the context in map/fetchOrder, never in verify (it has no context); prefer the transmission-signature check |
The tenant's secret is stored per connector (ecommerce_<id>_secret); the settings screen
renders the label you give. Extra fields (store hash, access token…) go in credentials and
arrive in ctx.credentials.
5. Pull mode#
When the webhook carries only an id (BigCommerce, Ecwid, Squarespace, Square), declare
fetchOrder(payload, ctx) — fetch with ctx.fetchImpl (never the global fetch) using
ctx.credentials, return the full order (or null if the platform says it does not exist),
throw on transport errors (the receiver answers 502 so the platform retries). Tests inject a fake
fetchImpl that returns the fixture.
6. Classification#
classify(payload, headers) returns "paid", "refund" or "ignore". Acknowledge (ignore)
everything you do not issue for — an order-created event before payment, a test ping, an
unrelated topic — instead of failing; a platform retrying a 4xx forever helps no one.
7. The file#
// src/ecommerce/connectors/defs/<id>.ts
/** <Platform> — <event>, <how it is signed>. Docs: <official URL>. */
import type { ConnectorDef } from "../types.js";
export const <id>: ConnectorDef = {
id: "<id>", label: "<Platform>", category: "store", mode: "hook", market: "global",
tagline: "<Hebrew one-liner>",
auth: { kind: "hmac", header: "x-...-signature", encoding: "hex", label: "<Hebrew label>" },
classify: (payload, headers) => ...,
map: (payload, ctx) => ({ externalId, apiIdentifier: `<id>-${externalId}`, input }),
mapRefund: ..., // optional, set supportsRefunds: true
setup: {
docsSlug: "<id>",
events: ["<event name>"],
steps: ["<Hebrew step 1>", "<step 2>", "<step 3>"],
sampleEvent: { /* a REAL-shaped payload from the official docs */ },
sampleHeaders: { "<event header>": "<value>" },
},
};Then register it — one import line and one list entry at the end of each block in
src/ecommerce/connectors/registry.ts (the @connectors: markers).
8. The test#
test/connectors-<group>.test.ts, in-process, no network:
- Mapping: lines, quantities, per-line VAT, shipping, discount, payment total; customer fields.
- Classification: paid vs ignored events.
- Signature: the real algorithm verifies and a one-byte change fails.
- End-to-end:
handleInbound(id, hookId, request, { trusted: true })issues a document (status: "issued"), and the same delivery twice returns the same document. - Refunds (if supported): a full and a partial refund produce credit notes referencing the
original; a refund for an order that never issued is recorded as
error. - Pull mode:
fetchImplinjected, transport failure ⇒ 502.
Run: npm run typecheck && npx vitest run test/connectors-<group>.test.ts test/connectors-framework.test.ts.
9. The wiki page#
docs/wiki/connectors/<id>.md — rendered at /developers/connectors/<id> on the developer
portal. Structure:
# <Platform>
> One-line summary (Hebrew).
## התקנה (5 דקות) ← the merchant steps, Hebrew, numbered, screenshots optional
## Developer reference ← English: event, auth, payload → document mapping table, refunds,
limits, test with curl (a signed sample against your own hookId)
## Troubleshooting ← Hebrew: 401 / 404 / "blocked" / "dormant" and what to doHebrew copy rules: no הינו/הינה, ניתן ל-, על מנת, מדובר ב-, מהווה, הנ"ל, בנוסף לכך, ברמה של. Keep code, field names and headers in English.
10. Definition of done#
validateRegistry()is empty (the framework test enforces it) andsampleEventmaps.- Tests + typecheck green; nothing outside your connector and its docs was edited (except the two registry lines).
- The wiki page exists and its
docsSlugmatches the file name.