A production-grade, fully-typed TypeScript client for the SumUp API — Checkouts, Readers, Customers, Payment Instruments, Transactions, Payouts, Receipts, Members, Memberships, Roles, and Merchants.
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.
npm install sumup-clientRequires Node.js >= 18.17 (for global fetch), or any runtime that
provides one (Bun, Deno, Cloudflare Workers, modern browsers).
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",
},
});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).
| 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.
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;
}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)`);
});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"));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.
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" },
});
},
});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).
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.
MIT. See LICENSE.