@gobtcpay/pos-api-sdk
v0.1.1
Published
GoMining POS API SDK
Readme
@gobtcpay/pos-api-sdk
Client for the GoBTC Pay POS API — accept Bitcoin payments from a point-of-sale terminal: create a payment, show the QR code, and track it until it settles on-chain.
Installation
npm install @gobtcpay/pos-api-sdkRuns on Node >= 18 and in browsers — it uses the isomorphic Web Crypto API, no
node:crypto dependency.
⚠️ The
apiKeyis a per-terminal secret. Only embed it in a trusted environment (server, or a controlled POS device). Do not ship it in a public web page.
Quick start
import { GoBTCPay } from "@gobtcpay/pos-api-sdk";
// Initialize once. The client signs every request for you with a fresh
// HMAC signature + timestamp — there is no token to manage or refresh.
const btcPay = new GoBTCPay({
apiKey: process.env.POS_API_KEY!,
posTerminalId: process.env.POS_TERMINAL_ID!,
});
// Create a payment and show the QR to the customer.
const payment = await btcPay.createPayment({
amount: 10,
currency: "USD",
description: "Order #1024",
});
console.log(payment.paymentId, payment.qrString);
// Poll for the outcome.
const latest = await btcPay.getPayment({ paymentId: payment.paymentId });
console.log(latest.status); // initiated | paid | cleared | expired | canceled | failedCommonJS works too:
const { GoBTCPay } = require("@gobtcpay/pos-api-sdk");Usage by environment
The package is isomorphic (ESM + CJS, Web Crypto) and runs on a server, in a bundled browser app, and in a plain HTML page.
⚠️ Where to put the
apiKey. It is a per-terminal secret used to sign requests. Keep it server-side, or on a controlled POS device. Do not ship it in a public website. For browser usage the API must also allow your origin via CORS — otherwise route calls through your own backend.
Server (Node.js / Bun / Deno)
ESM:
import { GoBTCPay } from "@gobtcpay/pos-api-sdk";
const btcPay = new GoBTCPay({
apiKey: process.env.POS_API_KEY,
posTerminalId: process.env.POS_TERMINAL_ID,
});
const payment = await btcPay.createPayment({ amount: 10, currency: "USD" });
console.log(payment.qrString);CommonJS:
const { GoBTCPay } = require("@gobtcpay/pos-api-sdk");A common pattern is to keep the secret on your server and expose thin
endpoints (POST /payments, GET /payments/:id) that the browser calls.
Browser (with a bundler — Vite, webpack, Next.js, …)
Install and import like any package; the bundler picks the ESM build:
import { GoBTCPay } from "@gobtcpay/pos-api-sdk";
const btcPay = new GoBTCPay({ apiKey, posTerminalId });
const payment = await btcPay.createPayment({ amount: 10, currency: "USD" });
// Watch for the result and update the UI live.
const watcher = btcPay.watchPayment({ paymentId: payment.paymentId });
watcher.onChange((s) => (statusEl.textContent = s.status));
watcher.onPaid(() => showPaidScreen());Plain HTML (no build step)
Load the ESM build straight from a CDN inside <script type="module">:
<!doctype html>
<html>
<body>
<pre id="out">creating…</pre>
<script type="module">
import { GoBTCPay } from "https://esm.sh/@gobtcpay/pos-api-sdk";
const btcPay = new GoBTCPay({
apiKey: "YOUR_TERMINAL_KEY", // test/controlled device only — this is a secret
posTerminalId: "YOUR_TERMINAL_ID",
// baseUrl: "https://pay.dev.gobtcpay.com/public/api/v1.1", // staging
});
const out = document.getElementById("out");
try {
const payment = await btcPay.createPayment({ amount: 1, currency: "USD" });
out.textContent = `paymentId: ${payment.paymentId}\nqr: ${payment.qrString}`;
const watcher = btcPay.watchPayment({ paymentId: payment.paymentId });
watcher.onChange((s) => (out.textContent += `\nstatus: ${s.status}`));
} catch (err) {
out.textContent = "error: " + err.message;
}
</script>
</body>
</html>The browser must be allowed to reach the API origin (CORS). If you hit
Failed to fetch/ a CORS error, proxy the requests through your backend and pointbaseUrlat that proxy.
Configuration
| Option | Required | Default | Description |
| --------------- | -------- | ---------------------------------------------------- | ---------------------------------------------------- |
| apiKey | yes | — | Per-terminal secret used to sign every request. |
| posTerminalId | no | — | Default terminal for createPayment. |
| baseUrl | no | https://api.gobtcpay.com/public/api/v1.1 | Override for staging/dev environments. |
| timeoutMs | no | 30000 | Per-request timeout. |
| fetch | no | global fetch | Custom fetch implementation. |
Methods
btcPay.createPayment({ amount, currency, description?, ttlSeconds?, externalId?, posTerminalId? });
btcPay.getPayment({ paymentId });
btcPay.watchPayment({ paymentId, intervalMs?, timeoutMs?, until?, immediate?, stopOnError? });Auto-polling
watchPayment() polls getPayment on an interval until the payment reaches a
final status (cleared / expired / canceled / failed). It returns a
PaymentPoller with a live state object, subscriptions, and a done()
promise. It starts automatically. The interval defaults to and is clamped to a
minimum of 3 seconds.
const watcher = btcPay.watchPayment({ paymentId, intervalMs: 3000 });
// Live state, readable at any time:
watcher.state; // { status, payment, error, attempts, isPolling, isFinal, ... }
// Subscriptions (each returns an unsubscribe function):
watcher.onChange((state) => render(state.status)); // any state change
watcher.onUpdate((payment) => {}); // every successful poll
watcher.onPaid((payment) => {}); // customer paid (not yet on-chain)
watcher.onSettled((payment) => {}); // reached a final status
watcher.onError((err) => {}); // a poll failed
// Or just await the outcome:
const finalPayment = await watcher.done();
watcher.stop(); // stop early
paidvscleared:paidmeans the customer has paid (the moment a cashier usually cares about);clearedis the later on-chain confirmation. By default the poller keeps running until a final status (cleared/ …) anddone()resolves there. To stop as soon as the customer pays, passuntil: ["paid"]— or just react toonPaidwhile polling continues.
Webhooks
Register a webhook URL in the merchant dashboard to receive
payment.status.updated events instead of (or in addition to) polling. The
handler verifies the X-GoBTCPay-Signature header, enforces a replay window,
and de-duplicates on eventId.
const webhooks = btcPay.webhooks({ signingSecret: process.env.POS_WEBHOOK_SECRET! });
webhooks.on("payment.status.updated", (event) => {
// event.data has the same shape as getPayment()
console.log(event.data.paymentId, event.data.status);
});
// Express example — pass the RAW request body, not a re-serialized object.
app.post("/webhooks/gobtcpay", express.raw({ type: "*/*" }), async (req, res) => {
try {
await webhooks.handle(req.body.toString("utf8"), req.header("X-GoBTCPay-Signature"));
res.sendStatus(200); // any 2xx acknowledges; non-2xx is retried
} catch (err) {
res.sendStatus(400);
}
});handle() returns the parsed event, or null if it was a duplicate.
Use constructEvent() if you only want to verify + parse without dispatching.
Errors
All errors extend GoBTCPayError:
GoBTCPayApiError— the API returned an error envelope or non-2xx status (.httpStatus,.body).WebhookSignatureError— a webhook signature failed verification.
Development
npm install
npm run build # dist/ (ESM + CJS + .d.ts)
npm run dev # rebuild on change
npm run typecheckVersioning
The API version is pinned inside the SDK (currently v1.1, exported as
API_VERSION) — you don't put it in a URL. The SDK always speaks the API
version it was built and tested against, and the package version tracks it via
semver:
| API version | SDK version |
| ----------- | ----------- |
| v1.x | 0.x / 1.x |
| v2 (breaking) | 2.x |
- Non-breaking API changes (
v1.1 → v1.2) ship as a minor SDK release. - A breaking API version (
v2) ships as a new major SDK release that points at the new base URL internally.
Need a different version or environment? Override baseUrl (the escape hatch):
new GoBTCPay({ apiKey, baseUrl: "https://api.gobtcpay.com/public/api/v1.1" });Testing
Two layers, both run on Vitest.
Unit tests — pure logic, no network. They gate every MR and run in CI.
npm test # run once
npm run test:watch # watch mode
npm run test:coverage # with a coverage report (thresholds enforced)They cover request signing (HMAC cross-checked against an independent implementation), the HTTP envelope transport, webhook verification (signature / replay window / de-dup), the payment poller lifecycle (fake timers), and the client surface.
Integration tests — exercise the library end-to-end against a live POS API
on the test contour. They are opt-in and self-skip unless credentials are
present, so npm test and CI stay green without secrets.
cp .env.example .env # fill in test-contour values, then:
POS_API_KEY=... POS_TERMINAL_ID=... npm run test:integrationSee .env.example for all variables. Use a test-contour
terminal secret only — never a production key.
License
MIT — a permissive open-source license. You may freely use, copy, modify, merge, publish, distribute, sublicense, and sell the software, including in closed-source and commercial products. The only condition is that the copyright notice and the license text are kept in copies. The software is provided "as is", without warranty and with no liability on the authors.
See LICENSE for the full text.
