sumup-client
v1.0.0
Published
A production-grade, fully-typed TypeScript client for the SumUp API (Checkouts, Readers, Customers, Transactions, Payouts, Receipts, Members, Memberships, Roles, Merchants) with retries, pagination, webhook handling, and RFC 9457 aware error handling.
Maintainers
Readme
sumup-client
A production-grade, fully-typed TypeScript client for the SumUp API — Checkouts, Readers, Customers, Payment Instruments, Transactions, Payouts, Receipts, Members, Memberships, Roles, and Merchants.
Why this exists
SumUp publishes official SDKs for Node, Go, Python, Java, .NET, PHP and Rust — but the Node one is a thin, loosely-typed wrapper. This package takes a stricter approach, modeling the API's real shape rather than papering over it:
- Zero runtime dependencies. Built entirely on native
fetch,Headers,URL, andAbortController— no axios, no request library, nothing to audit in your supply chain beyond this package itself. - Two error formats, normalized into one. Legacy resources
(Checkouts, Customers, Transactions, Payouts) return
{"error_code", "message", "param"}(sometimes as a list); newer resources (Readers, Members, Merchants) and every401return RFC 9457 Problem Details.SumUpErrordecodes both into one class, thrown so you get idiomatictry/catchor.catch()handling. - Two money formats, kept distinct. Legacy endpoints represent money
as a float major-unit amount (
FloatMoney); Readers use an integer minor-unitMinorUnitMoney. They're deliberately not unified into one type — doing so would either lose precision or require guessing a currency's decimal exponent. - Per-resource API versioning. Each resource method hits whatever
version SumUp actually ships for it (
v0.1for Checkouts,v2.1for Transaction reads butv1.0for refunds,v1for Merchants, etc.) rather than assuming one version for the whole client. - Reader checkouts modeled as genuinely async. Pushing a payment to a physical Solo reader resolves as soon as it's been sent to the device; this library returns that intermediate state honestly instead of faking synchronicity with an internal poll loop.
Installation
npm install sumup-clientRequires Node.js >= 18.17 (for global fetch), or any runtime that
provides one (Bun, Deno, Cloudflare Workers, modern browsers).
Quick start
import { SumUpClient } from "sumup-client";
const client = new SumUpClient({ apiKey: process.env.SUMUP_API_KEY });
const checkout = await client.checkouts.create({
checkoutReference: "order-1234",
amount: 10.5,
currency: "EUR",
merchantCode: "MC12345",
});
const paid = await client.checkouts.process(checkout.id, {
paymentType: "card",
card: {
name: "Jane Doe",
number: "4111111111111111",
expiryMonth: "12",
expiryYear: "2030",
cvv: "123",
},
});Configuration
const client = new SumUpClient({
apiKey: process.env.SUMUP_API_KEY, // or accessToken for OAuth2
maxRetries: 5,
receiveTimeoutMs: 15_000,
});See SumUpClientOptions in the type definitions for the full option
list (base URL override for testing, retry/backoff tuning, a custom
fetch implementation, extra headers, etc).
Resources
| Property | Covers |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| client.checkouts | create, get, find by reference, process (card/token/APM), deactivate, Apple Pay sessions, list payment methods |
| client.readers | pair, list, get (with If-Modified-Since support), get live status, rename/update, delete, push a checkout, terminate |
| client.customers | create, get, update |
| client.paymentInstruments | list, deactivate |
| client.transactions | get by id/code/foreign id/client id, list (with bracketed array filters), listAll() async-generator stream, refund |
| client.payouts | date-ranged report, JSON (list) or CSV (listCsv) |
| client.receipts | detailed receipt lookup |
| client.members | create, get, list, listAll(), update (full replace via PUT), delete |
| client.memberships | list the current authenticated user's own memberships (no merchant/user id in the path) |
| client.roles | create, list, get, update, delete custom roles |
| client.merchants | get profile (version + change-status pattern), list/get persons — read-only, SumUp's public API has no merchant-update endpoint |
client.webhooks isn't a REST resource — see Webhooks below.
Error handling
Every method throws a SumUpError (API/transport failures) or a
SumUpValidationError (bad arguments, caught before any request is
sent). SumUpError exposes a consistent set of fields (status, code,
message, param, type, title, instance, errors, raw) no
matter which underlying shape the API returned:
try {
await client.checkouts.process(checkoutId, { paymentType: "card", card });
} catch (error) {
if (error instanceof SumUpError && error.code === "CARD_DECLINED") {
return { declined: true, reason: error.message };
}
throw error;
}Retries and telemetry
429 and 5xx responses (and transport failures) are retried with
exponential backoff and jitter, honoring a numeric retry-after header
when SumUp sends one. Every request emits typed telemetry events you can
subscribe to without modifying this library:
client.on("request:success", ({ resource, operation, status, durationMs }) => {
logger.info(`sumup ${resource}.${operation} -> ${status} (${durationMs}ms)`);
});
client.on("request:retry", ({ resource, operation, attempt, delayMs }) => {
logger.warn(`sumup ${resource}.${operation} retrying (attempt ${attempt}, waiting ${delayMs}ms)`);
});Streaming large result sets
client.transactions.listAll() and client.members.listAll() return
async generators built on a generic paginate() helper, so you can use a
plain for await loop and stop whenever you like without fetching more
pages than you need:
for await (const tx of client.transactions.listAll("M1", { statuses: ["SUCCESSFUL"] })) {
if (tx.amount && tx.amount.amount > 100) {
console.log(tx.id, tx.amount.amount);
}
}Or collect everything into an array with the collect() helper:
import { collect } from "sumup-client";
const allMembers = await collect(client.members.listAll("M1"));Webhooks
SumUp's Checkout webhooks are not signed — there's no HMAC or
signature header to check for this API (that's a different SumUp product
entirely; see the Webhooks class doc comment for the full explanation).
SumUp's own recommendation is to treat the webhook body as an
unauthenticated pointer and re-fetch the real state from the API:
app.post("/webhooks/sumup", express.text({ type: "*/*" }), async (req, res) => {
try {
const checkout = await client.webhooks.handle(req.body);
await handleCheckoutUpdate(checkout);
} catch {
// still ack below, even on a parse/lookup failure
}
// Always ack quickly with 2xx, however processing went — SumUp retries
// non-2xx deliveries at 1 min, 5 min, 20 min, and 2 hours.
res.sendStatus(200);
});client.webhooks.parse() decodes the payload without trusting it;
client.webhooks.verify() does the recommended re-fetch;
client.webhooks.handle() does both in one call, as above.
Testing your own integration
This library's own test suite uses a small hand-rolled fetch stub (see
test/support/mock-fetch.ts) rather than a mocking library — inject your
own via the fetch client option:
const client = new SumUpClient({
apiKey: "sk_test_123",
fetch: async (url, init) => {
// return a Response however you like
return new Response(JSON.stringify({ id: "chk_1", status: "PAID" }), {
headers: { "content-type": "application/json" },
});
},
});Development
npm install
npm run ci # format check + lint + typecheck + test with coverage + buildIndividual scripts: npm run format, npm run lint, npm run typecheck,
npm test, npm run test:coverage, npm run build, npm run docs
(generates API docs into docs/ via TypeDoc).
A note on endpoint coverage
Every path, parameter, and response field in this library was
cross-checked against SumUp's published
OpenAPI spec — including some
easy-to-miss details it corrects for: client.memberships has no
merchant/user id in its path (it's always the calling user's own
memberships), client.transactions.refund() is scoped under
/merchants/{merchantCode}/payments/{id}/refunds and returns no body,
client.merchants has no update method in the public API, reader
checkouts use tipRates/tipTimeout rather than a flat tipAmount, and
transaction history's array filters (statuses[], paymentTypes[],
entryModes[], types[]) are sent as repeated bracketed query keys, not
comma-joined values. SumUp does still evolve its API surface over time,
so if you hit a mismatch, please open an issue or PR — every resource
module follows the same pattern, so adding or correcting an endpoint is
usually a small, self-contained change.
License
MIT. See LICENSE.
