Pure TypeScript helpers for SUMIT (formerly OfficeGuy) recurring billing and trigger webhooks. SUMIT routes card clearing through partner processors such as Upay, and their error codes surface in SUMIT responses — this package handles SUMIT's request/response shapes and redacts those upstream codes before they hit your logs. Zero runtime dependencies.
Companion package: sumit-react — <SumitCheckout /> plus Next.js charge and webhook route helpers.
- Why this package
- Install
- Build a one-off charge payload
- Build a recurring-charge payload
- Build a create-document payload
- Normalize a charge response
- Normalize a create-document response
- Normalize a SUMIT trigger / webhook payload
- Safety
- Development
- License
SUMIT (formerly OfficeGuy) does not publish a typed SDK for their billing APIs, and their trigger webhooks ship in three different content shapes. SUMIT also delegates the actual card clearing to partner processors (Upay is one — others exist), so processor-level error codes (e.g. Upay_30001419) appear unredacted inside SUMIT's response bodies. This package gives you a small, opinionated surface that is safe to drop into any backend:
- Build
/billing/recurring/charge/request payloads with strict types. - Normalize successful and failed charge responses into a single discriminated union.
- Parse SUMIT Trigger / Webhook payloads — JSON,
application/x-www-form-urlencoded, and SUMIT'sjson=…envelope. - Redact API keys, tokens, card data, emails, and other sensitive fields before logging.
See
docs/API_REFERENCE.mdfor a deeper summary of the SUMIT endpoints, response envelopes, trigger shapes, and redaction rules this package targets.
pnpm add sumit-api
# or
npm install sumit-api
# or
yarn add sumit-apiThe package has no runtime dependencies.
import { buildOneOffChargePayload } from "sumit-api";
const payload = buildOneOffChargePayload({
companyId: 123,
apiKey: process.env.SUMIT_API_KEY!,
customer: {
externalIdentifier: "org_123",
name: "Acme Ltd",
emailAddress: "billing@example.com",
},
singleUseToken: "[single-use-token-from-client]",
item: {
name: "Setup fee",
description: "One-time onboarding charge",
unitPrice: 49,
currency: "USD",
},
});POST this body to https://api.sumit.co.il/billing/payments/charge/. The response uses the same shape recurring charges return, so normalizeChargeResponse handles both — a one-off success surfaces as eventType: "payment.succeeded" (no recurringItemId).
import { buildRecurringChargePayload } from "sumit-api";
const payload = buildRecurringChargePayload({
companyId: 123,
apiKey: process.env.SUMIT_API_KEY!,
customer: {
externalIdentifier: "org_123",
name: "Acme Ltd",
emailAddress: "billing@example.com",
},
singleUseToken: "[single-use-token-from-client]",
item: {
name: "Pro Plan",
description: "Pro subscription — monthly",
unitPrice: 19,
currency: "USD",
durationMonths: 1,
},
});Issue a SUMIT accounting document (חשבון עסקה / Transaction Invoice) without charging a card — useful when a proposal/quote is accepted and you want to hand the customer a pre-payment invoice. POST the body to https://api.sumit.co.il/accounting/documents/create/.
import { buildCreateDocumentPayload, SUMIT_DOCUMENT_TYPE } from "sumit-api";
const payload = buildCreateDocumentPayload({
companyId: 123,
apiKey: process.env.SUMIT_API_KEY!,
documentType: SUMIT_DOCUMENT_TYPE.ProformaInvoice, // 3 — חשבון עסקה
customer: {
externalIdentifier: "client_42",
name: "Acme Ltd",
emailAddress: "billing@example.com",
taxId: "514999000", // ת.ז. / ח.פ. — mapped to CompanyNumber
},
items: [
{ name: "Logo design", description: "Includes 3 revisions", unitPrice: 1500, quantity: 1 },
{ name: "Development hours", unitPrice: 300, quantity: 8 },
],
currency: "ILS",
vatIncluded: false, // unit prices are net; SUMIT adds VAT
language: "he",
sendByEmail: { emailAddress: "billing@example.com" }, // optional
});SUMIT_DOCUMENT_TYPE covers SUMIT's Accounting_Typed_DocumentType enum — Invoice (0, חשבונית מס), InvoiceAndReceipt (1, חשבונית מס-קבלה), Receipt (2, קבלה), ProformaInvoice (3, חשבון עסקה), PriceQuotation (12, הצעת מחיר), credit/expense variants, and more. Pass any numeric code directly via documentType if needed.
language accepts either a SUMIT_LANGUAGE numeric code or the shorthand strings "he"/"en"/"ar"/"es" (and their full English names) — the helper converts to the numeric enum SUMIT requires. Unknown strings are dropped silently.
customer.searchMode is derived automatically when omitted: SUMIT id 1, ExternalIdentifier 2, otherwise 0 (create new). Pass an explicit value to override.
normalizeChargeResponse handles both one-off and recurring response shapes — a recurring.charged event is surfaced only when SUMIT returns a RecurringCustomerItemIDs[*]. (normalizeRecurringChargeResponse remains exported as an alias.)
import { normalizeChargeResponse } from "sumit-api";
const event = normalizeChargeResponse(sumitResponse);
if (event.ok && event.eventType === "recurring.charged") {
// Save event.customerId, event.recurringItemId, event.paymentId, event.documentId, ...
}
if (event.ok && event.eventType === "payment.succeeded") {
// One-off charge succeeded — store event.paymentId and event.documentId.
}
if (event.ok === false) {
// Don't activate the subscription / mark the order paid. Store event.diagnostic safely.
}A successful SUMIT charge response typically includes:
| Field | Meaning |
|---|---|
Payment.ValidPayment === true |
Provider considers the charge valid |
Payment.Status === "000" |
Provider success status code |
RecurringCustomerItemIDs[0] |
The created recurring-item ID |
CustomerID |
SUMIT customer record ID |
DocumentID |
Issued invoice / receipt ID |
import { normalizeCreateDocumentResponse } from "sumit-api";
const event = normalizeCreateDocumentResponse(sumitResponse);
if (event.ok && event.eventType === "document.created") {
// Persist event.documentId / event.documentNumber / event.documentDownloadUrl.
}
if (event.eventType === "document.failed") {
// event.userErrorMessage is safe to display; event.technicalErrorDetails is redacted.
}A successful create-document response surfaces:
| Field | Source |
|---|---|
documentId |
Data.DocumentID / DocumentID |
documentNumber |
Data.DocumentNumber / Data.Document.Number |
documentDownloadUrl |
Data.DocumentDownloadURL / Data.Document.DownloadURL |
customerId |
Data.CustomerID / Data.Customer.ID |
import { normalizeSumitIncomingPayload } from "sumit-api";
const normalized = normalizeSumitIncomingPayload(payloadOrUrlSearchParams);
if (normalized.eventType === "sumit.trigger.unmapped") {
// Store the sanitized raw event for later mapping.
}SUMIT Trigger webhooks are often view / card based and may not include a fixed provider event schema — this package does not assume Stripe-style lifecycle events.
For SUMIT view-shaped trigger payloads with top-level Folder, EntityID, Type, and Properties, normalization extracts these safe reconciliation fields when present:
| Normalized field | Source |
|---|---|
paymentId |
EntityID |
customerId |
Properties.Property_3[0].ID |
documentId |
Properties.Property_5[0].ID |
amount |
Properties.Billing_Amount[0] |
status |
Type |
occurredAt |
Properties.Property_2[0] |
These events still normalize as sumit.trigger.unmapped until your application explicitly authenticates and maps them to a trusted billing lifecycle event.
Never log or persist raw SUMIT payloads. Use
redactSumitPayload, or persist only the safe normalized fields.
redactSumitPayload walks the value tree and masks sensitive data via two complementary mechanisms:
| Mechanism | Catches |
|---|---|
Key-based (SENSITIVE_KEY_PATTERN) |
API keys, public keys, single-use tokens, card mask/pattern/token/expiration, citizen ID, card-owner name and social ID, Authorization, secrets, passwords, CVV, email addresses, phone, document download URLs, full CreditCard_* and DirectDebit_* subtrees. |
Text-based (redactSensitiveText) |
Embedded emails, "Credit Card (1234)" patterns, token=… / apikey=… key-value strings, Upay_* references, UUIDs, 12–19 digit card-like numbers in free text, citizen IDs in keyword context (citizen, ת.ז, מ.ז). |
Form payloads parsed by normalizeSumitIncomingPayload reject prototype-pollution keys (__proto__, constructor, prototype) before assembling the nested object — see src/index.ts.
pnpm install
pnpm test # vitest run
pnpm typecheck # tsc --noEmit
pnpm build # tsc → dist/The build emits ESM with .d.ts declarations to dist/. Source lives in src/.