npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

saman-payment-pos

v1.0.1

Published

TypeScript SDK for the SSP1126 (Saman Electronic Payment) POS terminal — ISO-8583 over TCP/Serial

Readme

SSP1126 (Saman Electronic Payment)

npm npm GitHub documentation Build, Test and Publish

A clean, dependency-free TypeScript SDK for the SSP1126 POS terminal (Saman Electronic Payment). It speaks the terminal's native ISO-8583:1987 over TCP protocol, reverse-engineered from the legacy Delphi component.

Installation

npm install --save saman-payment-pos

Quick start

import { Ssp1126Client } from 'saman-payment-pos';

// TCP (default channel)
const pos = new Ssp1126Client({ host: '192.168.14.105', port: 1197 });

const conn = await pos.connectionTest();
if (conn.ok) {
  // No purchaseId — that would request an *identified purchase*; see below.
  const res = await pos.purchase({ mainAmount: 10000, referenceData: 'ORDER-1001' });
  console.log(res.ok, res.responseCode, res.responseMessage, res.rrn);
}
pos.close();

Choosing a transport

The same client works over TCP or Serial — pick the channel in the constructor options.

// TCP: { host, port? }
const tcp = new Ssp1126Client({ transport: 'tcp', host: '192.168.14.105', port: 1197 });
//   `transport` may be omitted; TCP is the default.

// Serial: { path, baudRate? }  — 8 data bits, 1 stop bit, no parity (fixed)
const serial = new Ssp1126Client({ transport: 'serial', path: '/dev/ttyUSB0', baudRate: 19200 });
//   Windows: path: 'COM3'

Serial uses the optional serialport package; install it only if you use the serial channel (npm install serialport). TCP has zero runtime dependencies. Both channels frame, buffer partial reads, time out and surface errors identically (shared FrameTransport base), so they are fully interchangeable.

You can also build a transport directly:

import { createTransport, TcpTransport, SerialTransport } from 'saman-payment-pos';

const t1 = createTransport({ host: '192.168.14.105' }); // TcpTransport
const t2 = createTransport({ transport: 'serial', path: 'COM3' }); // SerialTransport

API

Construct with new Ssp1126Client(options):

| option | default | meaning | | ------------------ | ----------- | -------------------------------------------------------- | | transport | "tcp" | channel: "tcp" or "serial" | | host | — | terminal IP (TCP) | | port | 1197 | terminal TCP port (TCP) | | path | — | serial device, e.g. "COM3" / "/dev/ttyUSB0" (serial) | | baudRate | 19200 | serial baud rate (serial) | | connectTimeoutMs | 10000 | connect / open timeout | | currency | "364" | DE49 currency (IRR) | | componentVersion | "1.4.3.0" | DE57 for purchases | | verifyMac | true | verify DE64 MAC on responses | | logger | — | (line) => void raw frame / step trace | | minReconnectGapMs| 500 | reconnect gap after a transaction that ended cleanly (final 17) | | reconnectGapAfterPartialMs | 1500 | reconnect gap after one that ended without a final 17 | | firstAckTimeoutMs| 3000 | timeout for the opening 15 acknowledgement only | | retryOnFirstTimeout | true | retry once if the opening request of a transaction times out on a fresh connection | | retryDelayMs | 500 | delay before the retry above |

Each method runs a full transaction (connect → multi-step dialog → dispose → close) and resolves to a TransactionResult (or a typed result for reports/ops):

| method | purpose | | ------------------------------------------------------- | ------------------------------------------------ | | connectionTest() | reachability / handshake | | getAuthorizedOperations() | which operations the terminal is provisioned for | | balance() | card balance inquiry | | purchase({ mainAmount, … }) | PC-initiated purchase (amount known up front) | | posStarterPurchaseInit() | step 1 of a POS-initiated purchase: read the card, list routing options | | posStarterPurchaseFin({ mainAmount, segment?, … }) | step 2: complete a purchase started with posStarterPurchaseInit() | | posStarterPurchaseCancel() | abort a posStarterPurchaseInit() you're not completing | | billPayment({ billId, paymentId, … }) | pay a bill | | billRequest() | read a bill from the inserted card | | pinCharge({ … }) | buy a PIN charge voucher | | topupCharge({ mobileNumber, … }) | direct mobile top-up | | mciBillInquiry({ mciNumber, billType }) | MCI (Hamrah) bill inquiry | | tciBillInquiry({ tciNumber, billType }) | TCI (telecom) bill inquiry | | totalReport({ fromDate, toDate, posPin? }) | aggregate totals | | report({ filter, filterValues, reportType, posPin? }) | detailed rows |

Bill identifier helpers (exported separately, no terminal needed): validateBill(billId, paymentId), billAmountRials(paymentId), billCategory(billId).

TransactionResult fields: ok, responseCode, responseMessage, terminalId, traceNumber, serialId, rrn, amount, effectiveAmount, transactionDate, cardMask, cardHash1, cardHash2, charge voucher fields, plus fields (every raw DE harvested), timedOut (true if a step never got a response at all, as opposed to a decline — see "Connection behavior" below).

purchaseId / DE63 — the usual cause of 07

Do not set purchaseId for an ordinary sale. It populates DE63, which asks the terminal for an identified purchase (خرید شناسه‌دار) — a distinct transaction type the vendor added in v1.0.1 of the web service, provisioned separately from ordinary purchase. A merchant without that entitlement answers responseCode: '07' ("No permission for this operation"), immediately and before it even prompts for a card.

Because DE63 has no bit of its own in DE48, getAuthorizedOperations() cannot warn you: a terminal can report every flag true and still refuse every DE63-bearing purchase. Verified end-to-end against a live terminal — four purchase variants carrying a purchaseId all returned 07; the same terminal completed a normal sale (00) the moment DE63 was omitted.

If you want your own order number on the transaction, use referenceData (DE56) or additionalData (DE48):

await client.purchase({
  mainAmount: 10000,
  referenceData: 'ORDER-1234', // your reference — NOT purchaseId
});

Reserve purchaseId for the case where you actually mean an identified purchase and know the merchant is entitled to it.

PC-started vs. POS-started purchase

Both flows exist and are independently provisioned, so getAuthorizedOperations().purchase is only a hint — let DE39 on the real request be the authority. If you get 07, check purchaseId first (above); that is far more often the cause than a genuinely disabled flow.

  • purchase() — PC-started. The PC sends the amount immediately; the customer taps/inserts their card only after that, within the same request's financial step.
  • posStarterPurchaseInit() + posStarterPurchaseFin() — POS-started. The customer taps/inserts their card first (Init), the terminal reports back which payment routing options that card supports (usually just one), and only then does the PC send the amount to complete the sale (Fin). This mirrors a point-of-sale flow where the cashier doesn't know the amount will be finalized until the card is already presented.
const init = await pos.posStarterPurchaseInit(); // prompts customer to tap/insert card
if (init.ok) {
  // init.segments is usually a single { code, label } pair; if there's more than
  // one, let the customer/cashier pick and pass its `code` as `segment` below.
  const segment = init.segments.length > 1 ? init.segments[0].code : undefined;
  const fin = await pos.posStarterPurchaseFin({ mainAmount: 10000, segment });
  console.log(fin.ok, fin.responseCode, fin.rrn);
} else {
  console.log('card read failed:', init.responseCode, init.responseMessage);
}

posStarterPurchaseInit() deliberately leaves the connection open on success (mirroring the reference component) so posStarterPurchaseFin() can complete the same transaction on it — don't call any other method on this client in between, or call posStarterPurchaseCancel() first. If Init succeeds but you decide not to complete the sale (customer backs out, UI abandoned, timeout in your own app), call posStarterPurchaseCancel() to dispose and close cleanly rather than leaving the connection open indefinitely.

Wire framing

The framing is asymmetric, which is the easiest thing to get wrong:

PC  → POS:  [ISO-8583 message]                                        (raw, nothing added)
POS → PC :  [2-byte big-endian length][5-byte header 60 00 00 00 00][ISO-8583 message]

On responses the length counts the header + ISO bytes. The SDK strips that envelope on receive and adds nothing on send; you always work with the bare Iso8583Message.

Both reference implementations agree: the Delphi SendCommand transmits the MACPack output verbatim — every capture in SSP1126 PC to POS 2007/log/*.txt logs Send Command With Result : N with N equal to the message length exactly — and the C# SSP1126.PcPos.dll (NetworkChannel) makes sendMessageHeader a no-op, never sends a length, and sets setHeader(new byte[0]), while its read path skips exactly 7 bytes.

Prefixing a request with the response envelope shifts every field by 7 bytes. The terminal then can't parse the request and answers immediately with a well-formed rejection — DE39=98 ("Operation cancelled by user"), DE3=000000 — which looks deceptively like the customer pressed Cancel.

Connection behavior

Every transaction opens a fresh TCP connection, per the reference component. The terminal is not ready to serve a new connection immediately after the previous one closes: it completes the TCP handshake but never answers the first request.

How long it needs depends on how the previous transaction ended. A transaction that received the terminal's final 17 recovers quickly; one that stopped short (getAuthorizedOperations(), totalReport(), report(), or anything that failed) needs substantially longer. The SDK tracks this and picks the gap automatically:

  • minReconnectGapMs (default 500) — after a clean 17. Measured on firmware 10.063.00PO: 0 ms and 250 ms stalled, 500 ms and above did not.
  • reconnectGapAfterPartialMs (default 1500) — after a transaction with no final 17. Measured by running getAuthorizedOperations() then a connection test: 500 ms and 1000 ms both stalled (~12–13 s end to end), 1500 ms completed in ~3 s.

Two more settings bound the cost when it still goes wrong:

  • firstAckTimeoutMs (default 3000) — the opening 15 arrives in 3–5 ms when it is coming at all, so waiting 10 s only delays discovering the terminal is not listening. This covers the 15 only: the terminal sends it immediately and then waits for a card, so a short value here does not shorten the customer's card window.
  • retryOnFirstTimeout (default true) — safety net. If the opening response still times out on a freshly-opened connection, the SDK closes, waits retryDelayMs (default 500 ms), reconnects and resends the opening request exactly once.

A timeout on an already-warm connection, or any response after the first, is never retried — that's a real failure. Check result.timedOut to tell a timeout apart from a decline.

close() hard-closes the socket once the final Dispose is flushed. A graceful half-close is not enough: the terminal never sends FIN back, so the connection would park in FIN-WAIT-2 and the open handle would keep Node's event loop alive — a script that had finished its work would never exit.

Low-level building blocks (also exported)

  • Iso8583MessagesetStr/setBin/getStr/getBin, packWithMac(), Iso8583Message.unpack()
  • computeMac(body) / verifyMac(rawMessage) — the DE64 DES CBC-MAC
  • TcpTransport / SerialTransport — framing-aware channels behind the Transport interface (connect, send, receive, clearInbox, close); createTransport(config) builds either
  • desCbcEncrypt is internal; use computeMac for the MAC

Notes & limitations

  • One transaction per connection. The client opens, runs, sends Dispose, and closes — exactly like the reference component. Don't hold a client across transactions; just call the next method.
  • Both TCP and Serial transports ship behind one Transport abstraction and are selected by the transport option. Serial is COMx//dev/tty*, 8-N-1, default baud 19200 (see PROTOCOL.md §1) and uses the optional serialport package.
  • The DE64 MAC uses a fixed shared key that shipped in the original source. It is an integrity check, not encryption. Operate the link on a trusted network.
  • Response-code text is provided in English (the original Persian strings were lost to a code-page conversion in the source dump).