swish-mock
v0.1.0
Published
Drop-in mock server for the Swedish Swish Merchant API. Runs locally with no certificates, supports configurable scenarios via phone-number suffix (matching MSS), and is suitable for CI and local development.
Maintainers
Readme
swish-mock
A drop-in mock server for the Swedish Swish Merchant API. Runs locally with no certificates, drives outcomes from the payer phone number (matching the MSS convention), fires real callbacks, and is suitable for CI and local development.
Tested against swish-merchant
(the published Swish Merchant client on npm) — the integration tests in this
repo run a real Swish instance against the mock and assert a full create →
poll → terminal-state → callback flow.
Why
Swish has an official simulator (MSS), but it requires:
- A publicly reachable HTTPS callback URL — ngrok or similar for local dev.
- Network round-trips that introduce latency and flakiness in CI.
- Sharing a globally rate-limited test endpoint with everyone else.
This package runs entirely in-process, resolves payments deterministically, and lets you assert callback contents without an HTTP listener.
Install
npm install --save-dev swish-mockQuickstart — CLI
npx swish-mockStarts a server on http://127.0.0.1:8586 with the three Merchant endpoints:
PUT /api/v2/paymentrequests/{instructionUUID}
GET /api/v1/paymentrequests/{id}
PATCH /api/v1/paymentrequests/{id}Environment variables
| Var | Default | Description |
|---|---|---|
| PORT / SWISH_MOCK_PORT | 8586 | Listen port |
| HOST / SWISH_MOCK_HOST | 127.0.0.1 | Listen host |
| SWISH_MOCK_SCENARIO | paid | Default scenario when no header / phone match |
| SWISH_MOCK_RESOLVE_DELAY_MS | 1500 | ms before a payment resolves to terminal |
| SWISH_MOCK_ORDER_TTL_MS | unset | Evict payments older than this many ms |
| SWISH_MOCK_BODY_LIMIT | 100kb | Maximum JSON request body size |
Invalid values exit with a clear error rather than crashing on NaN.
Quickstart — programmatic
import { createMockServer } from "swish-mock";
const captured: unknown[] = [];
const { app, flush } = createMockServer({
defaultScenario: "paid",
resolveDelayMs: 0,
// Capture callbacks in tests without spinning up an HTTP listener
onCallback: (url, payload) => captured.push({ url, payload }),
});
app.listen(8586);Scenarios
Five scenarios drive the terminal state:
| Scenario | Final status | errorCode |
|---|---|---|
| paid | PAID | — |
| declined | DECLINED | — |
| cancelled | CANCELLED | — |
| error | ERROR | BANKIDCL |
| timeout | ERROR | TM01 |
Routing precedence (highest first)
Phone-suffix on
payerAlias— last digit drives outcome (matches MSS):| Suffix | Scenario | |---|---| |
0|paid| |1|declined| |2|error| |3|timeout| |4|cancelled|x-mock-scenarioHTTP header — fallback when nopayerAliasor suffix is unmapped.Server
defaultScenario— used when neither phone nor header matches.
# Resolves to DECLINED (suffix wins)
curl -X PUT http://127.0.0.1:8586/api/v2/paymentrequests/11A86BE70EA346E4B1C39C874173F088 \
-H "Content-Type: application/json" \
-d '{
"payeeAlias": "1231181189",
"payerAlias": "46701112231",
"amount": "100",
"currency": "SEK",
"message": "Test"
}'State machine
A payment starts in CREATED and transitions exactly once to one of
PAID, DECLINED, CANCELLED, ERROR after resolveDelayMs. Merchant
cancellation via PATCH is only valid from CREATED — terminal states
return 422 RP09.
When the payment reaches its terminal state, the mock POSTs the
serialized payment to callbackUrl (if provided). Tests that don't want
real HTTP can supply onCallback to capture deliveries in-process.
Endpoints
Create payment
PUT /api/v2/paymentrequests/{instructionUUID}instructionUUID is 32 uppercase hex chars (no hyphens), client-generated.
Returns 201 Created with Location and PaymentRequestToken headers and
an empty body — the same as real Swish.
Get payment
GET /api/v1/paymentrequests/{id}Returns the full payment object, including current status, errorCode,
paymentReference, and datePaid when applicable.
Cancel payment
PATCH /api/v1/paymentrequests/{id}
Content-Type: application/json
[{ "op": "replace", "path": "/status", "value": "cancelled" }]Only valid while the payment is CREATED.
Use with swish-merchant
Point the published Swish Merchant client at the mock by overriding url
and dropping its httpsAgent:
const Swish = require("swish-merchant");
// `cert`/`key` strings ≥ 1264 chars are passed through inline (the client
// otherwise treats them as filesystem paths).
const DUMMY = "x".repeat(1300);
const client = new Swish({
alias: "1231181189",
paymentRequestCallback: "https://merchant.example/cb",
cert: DUMMY,
key: DUMMY,
});
client.url = "http://127.0.0.1:8586";
client.httpsAgent = undefined; // node-fetch otherwise honors the dummy agent
const created = await client.createPaymentRequest({
phoneNumber: "46701112230", // ends in 0 → paid
amount: 100,
message: "Order 42",
});See tests/integration.test.ts for the full
flow including callback assertions.
Use in tests
import { createMockServer } from "swish-mock";
import request from "supertest";
const { app, flush, store } = createMockServer({ resolveDelayMs: 60_000 });
await request(app)
.put("/api/v2/paymentrequests/11A86BE70EA346E4B1C39C874173F088")
.send({ payeeAlias: "1231181189", amount: "100", currency: "SEK" });
await flush(); // resolve everything synchronously, bypass timers
const payment = store.get("11A86BE70EA346E4B1C39C874173F088");
expect(payment?.status).toBe("PAID");CI example
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx swish-mock &
- run: sleep 1 && npm test
env:
SWISH_BASE_URL: http://127.0.0.1:8586What the mock does not do
- No mTLS — the mock accepts plain HTTP. Bypass
httpsAgent/cert/keyin your client when pointing at the mock. - No QR image rendering — the
PaymentRequestTokenheader is returned; bring your own QR library if you need to render it. - No refunds endpoint — only the three core paymentrequest endpoints are mocked.
- No persistence — payments live in memory and are wiped on process exit.
- No real callback retries — same as real Swish.
Maintained by
Built and maintained by Fiive.
License
MIT — see LICENSE.
