חשבוניות SLK

Building a connector (contributor guide)

On this page

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#

ModeWhenWhat you write
hookThe platform can POST a webhook to a URLA ConnectorDef in src/ecommerce/connectors/defs/<id>.ts
pluginOur code runs inside the platform (WordPress, PrestaShop…)A plugin under integrations/<platform>/ that calls the REST API
recipeA 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 — the ConnectorDef contract (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.
  • taxIncluded states whether line prices are gross. Derive per-line vatBp from the platform's own tax data when it exists (Shopify tax_lines[].rate, WooCommerce total_tax/total); a line with no tax data inherits the document vatBp (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. mapRefund returns amountAgorot for a partial refund, null for a full one.
  • Customer name falls back to "לקוח/ה"; email/address are optional. VAT id (ח.פ./ת.ז.) goes in customerUniqueId when the platform has it.

4. Authentication — pick the strongest the platform offers#

Platform signs withauth
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#

ts
// 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:

  1. Mapping: lines, quantities, per-line VAT, shipping, discount, payment total; customer fields.
  2. Classification: paid vs ignored events.
  3. Signature: the real algorithm verifies and a one-byte change fails.
  4. End-to-end: handleInbound(id, hookId, request, { trusted: true }) issues a document (status: "issued"), and the same delivery twice returns the same document.
  5. 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.
  6. Pull mode: fetchImpl injected, 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 do

Hebrew copy rules: no הינו/הינה, ניתן ל-, על מנת, מדובר ב-, מהווה, הנ"ל, בנוסף לכך, ברמה של. Keep code, field names and headers in English.

10. Definition of done#

  • validateRegistry() is empty (the framework test enforces it) and sampleEvent maps.
  • Tests + typecheck green; nothing outside your connector and its docs was edited (except the two registry lines).
  • The wiki page exists and its docsSlug matches the file name.
Building a connector (contributor guide) | פורטל המפתחים — חשבוניות SLK