mcp-nexi-pos-api
v0.1.4
Published
MCP server for Nexi POS card payments
Readme
mcp-nexi-pos-api
MVP MCP server for Nexi POS payment operations.
It provides tools to create purchases and refunds, poll payment state, confirm transactions, recover unconfirmed transactions, check terminal status, and list terminal events.
Setup
From npm package
npm install -g mcp-nexi-pos-api
mcp-nexi-pos-apiFrom source
npm install
npm run build
npm startMinimum working config
You need Nexi POS API credentials before you can use the server:
NEXI_POS_API_KEY_IDNEXI_POS_API_KEY_SECRET- a Nexi POS terminal ID
The terminal ID is generated together with the API key ID and API key secret. Credential and terminal generation is outside this MCP server for now. Get these values from Nexi API documentation before starting the server.
The smallest useful .env file is:
NEXI_POS_API_KEY_ID=your-api-key-id
NEXI_POS_API_KEY_SECRET=your-api-key-secret
NEXI_POS_TERMINAL_ID=your-terminal-idNEXI_POS_TERMINAL_ID is optional. It is used as an environment fallback when a tool call does not include terminal_id and no session terminal has been set with set_terminal_id. It has no default value.
A fuller local .env file can include:
NEXI_POS_API_KEY_ID=your-api-key-id
NEXI_POS_API_KEY_SECRET=your-api-key-secret
NEXI_POS_TERMINAL_ID=your-terminal-id
NEXI_POS_BASE_URL=https://api.sandbox.npay.eu/pos/v1
NEXI_POS_DEFAULT_CURRENCY=SEK
NEXI_POS_MAX_AMOUNT_MINOR=100021
NEXI_POS_USER_AGENT=mcp-nexi-pos-api/0.1.4
NEXI_POS_STORAGE_PATH=./data/nexi-pos.sqlite
NEXI_POS_REQUEST_TIMEOUT_SECONDS=30Environment variables
| Variable | Description | Default |
| --- | --- | --- |
| NEXI_POS_API_KEY_ID | Nexi Basic Auth username. Generated outside this MCP server. | Required |
| NEXI_POS_API_KEY_SECRET | Nexi Basic Auth password. Generated outside this MCP server. | Required |
| NEXI_POS_TERMINAL_ID | Optional terminal ID fallback when no tool terminal_id or session terminal ID is available. Generated with the API credentials. | No default |
| NEXI_POS_BASE_URL | Nexi POS API base URL. | https://api.sandbox.npay.eu/pos/v1 |
| NEXI_POS_DEFAULT_CURRENCY | Currency used when a tool omits currency. | SEK |
| NEXI_POS_MAX_AMOUNT_MINOR | Safety limit in minor units. Defaults high enough for Nexi test-case amounts. | 100021 |
| NEXI_POS_USER_AGENT | User agent sent to Nexi. | mcp-nexi-pos-api/0.1.4 |
| NEXI_POS_STORAGE_PATH | SQLite file for local recovery state. | ./data/nexi-pos.sqlite |
| NEXI_POS_REQUEST_TIMEOUT_SECONDS | Hard timeout for each HTTP request to Nexi. | 30 |
Claude Desktop config
Minimum example for a global npm install:
{
"mcpServers": {
"nexi-pos": {
"command": "mcp-nexi-pos-api",
"env": {
"NEXI_POS_API_KEY_ID": "your-api-key-id",
"NEXI_POS_API_KEY_SECRET": "your-api-key-secret",
"NEXI_POS_TERMINAL_ID": "your-terminal-id"
}
}
}
}Fuller example for a global npm install:
{
"mcpServers": {
"nexi-pos": {
"command": "mcp-nexi-pos-api",
"env": {
"NEXI_POS_API_KEY_ID": "your-api-key-id",
"NEXI_POS_API_KEY_SECRET": "your-api-key-secret",
"NEXI_POS_TERMINAL_ID": "your-terminal-id",
"NEXI_POS_BASE_URL": "https://api.sandbox.npay.eu/pos/v1",
"NEXI_POS_DEFAULT_CURRENCY": "SEK",
"NEXI_POS_MAX_AMOUNT_MINOR": "100021",
"NEXI_POS_STORAGE_PATH": "/absolute/path/to/data/nexi-pos.sqlite"
}
}
}
}Example from source:
{
"mcpServers": {
"nexi-pos": {
"command": "node",
"args": ["/absolute/path/to/mcp-nexi-pos-api/dist/index.js"],
"env": {
"NEXI_POS_API_KEY_ID": "your-api-key-id",
"NEXI_POS_API_KEY_SECRET": "your-api-key-secret",
"NEXI_POS_TERMINAL_ID": "your-terminal-id"
}
}
}
}Amount format
All amounts are integers in ISO 4217 minor units. Do not send floating point values.
Examples:
SEK 1.00=100EUR 1.00=100JPY 1=1
The default max amount is 100021, meaning SEK/EUR 1000.21. This allows the documented Nexi simulator/test-case amounts. Set NEXI_POS_MAX_AMOUNT_MINOR to a lower value if your environment should block larger test amounts.
External IDs
external_id is your unique order or payment ID. Use a value that lets you find the payment later, for example order-1001-payment-1.
Rules:
- Use one unique
external_idfor one transaction. - Reuse the same
external_idonly when retrying the same transaction after a timeout, restart, or network problem. - Use a new
external_idfor a new order, a new payment attempt, or a different amount.
Reusing an old external_id for a different amount or order can return the old transaction or fail because Nexi treats it as the same transaction request.
Tools
set_terminal_id- store a terminal ID for this MCP server session.get_session_terminal_id- show the current session terminal ID.clear_terminal_id- clear the session terminal ID.list_test_cases- list documented Nexi simulator/test cases and their trigger amounts.create_purchase- low-level purchase call. Can userequested_amountor namedtest_case.take_payment- recommended normal purchase flow. It creates/polls a purchase until Nexi leavesPROCESSINGor the timeout is reached. Can userequested_amountor namedtest_case.create_refund- low-level refund call.confirm_transaction- confirm a purchase or refund result.get_transaction- fetch a transaction byexternal_idand terminal.get_unconfirmed_transactions- list unconfirmed transactions for a terminal.get_terminal_status- fetch terminal connection, transaction state, screen, and battery status.list_terminal_events- list terminal event stream entries, optionally filtered by event type.
Response guide
All tool responses use this JSON shape:
{
"ok": true,
"operation": "take_payment",
"terminal_id": "terminal-1",
"external_id": "order-123",
"state": "AWAITING_CONFIRM",
"result_code": "SUCCESS",
"result_description": "Approved",
"success": true,
"message": "Payment flow stopped at terminal/current state",
"user_message": "The card step succeeded, but the transaction is not complete until it is confirmed.",
"next_action": "Call confirm_transaction to complete the transaction.",
"summary": {
"amount": 500,
"currency": "SEK",
"state": "AWAITING_CONFIRM",
"result_code": "SUCCESS",
"reference": "reference-from-nexi",
"masked_card": "************0010"
},
"transaction": {},
"raw": {}
}Field meanings:
ok: the MCP tool call was handled. It does not always mean that the card payment succeeded.success: the payment/refund result was successful. This is true only whenresult_codeisSUCCESS.state: the current Nexi transaction state, for examplePROCESSING,AWAITING_CONFIRM,CONFIRMED, orCOMMITTED.result_code: the payment/refund result from Nexi, when available.summary: short user-friendly payment details. Use this before inspectingraw.user_message: important human-readable warning or context.next_action: suggested next step, for example poll again, confirm, or no action needed.test_case: included whenrequested_amountmatches a documented Nexi simulator/test case, or whentest_casewas used as input.raw: full Nexi response for debugging and integrations.
Normal payment flow
For most clients, use take_payment for purchases. It calls Nexi purchase repeatedly while the state is PROCESSING, until the total timeout_seconds is reached or Nexi returns a later state such as AWAITING_CONFIRM, CONFIRMED, or COMMITTED.
You can provide the terminal in three ways:
- pass
terminal_idin the tool call; - call
set_terminal_idonce for the session; - set
NEXI_POS_TERMINAL_IDas an environment fallback.
Recommended flow:
- Start a payment with
take_payment. - If the response state is
AWAITING_CONFIRM, callconfirm_transactionwhen your business flow is ready. - If the response is still
PROCESSING, retry later with the sameexternal_idand terminal ID.
Examples
Take a 5 SEK payment
Tool: take_payment
{
"external_id": "order-1001-payment-1",
"requested_amount": 500,
"currency": "SEK",
"timeout_seconds": 60
}Run a named Nexi test case
Tool: take_payment
{
"external_id": "test-issuer-error-1",
"test_case": "issuer_error",
"currency": "SEK",
"timeout_seconds": 60
}The tool maps issuer_error to requested_amount: 100006. You can also use descriptions or aliases, for example "bank declined".
To list supported names and descriptions, call list_test_cases:
{}Confirm payment
Tool: confirm_transaction
{
"external_id": "order-1001-payment-1",
"result_code": "SUCCESS"
}Refund 5 SEK
Tool: create_refund
{
"external_id": "order-1001-refund-1",
"requested_amount": 500,
"currency": "SEK",
"original_purchase_external_id": "order-1001-payment-1"
}Purchase with environment terminal
Set NEXI_POS_TERMINAL_ID in the environment, then omit terminal_id in the tool call.
Tool: create_purchase
{
"external_id": "order-1002-payment-1",
"requested_amount": 500,
"currency": "SEK"
}Purchase with explicit terminal
Tool: create_purchase
{
"terminal_id": "your-terminal-id",
"external_id": "order-1003-payment-1",
"requested_amount": 500,
"currency": "SEK"
}Recovery after restart
If the server restarts during a payment, use the same external_id and terminal ID.
Tool: get_transaction
{
"terminal_id": "your-terminal-id",
"external_id": "order-1001-payment-1"
}Then check unconfirmed transactions.
Tool: get_unconfirmed_transactions
{
"terminal_id": "your-terminal-id"
}You can also check the terminal status.
Tool: get_terminal_status
{
"terminal_id": "your-terminal-id"
}If a transaction is waiting for confirmation, confirm it.
Tool: confirm_transaction
{
"terminal_id": "your-terminal-id",
"external_id": "order-1001-payment-1",
"result_code": "SUCCESS"
}List terminal events
Tool: list_terminal_events
{
"terminal_id": "your-terminal-id",
"event_type": "eu.npay.api.pos.v0.TerminalStatus",
"limit": 20,
"wait_seconds": 30
}If wait_seconds is omitted, the tool uses short polling with 0. Set a positive wait_seconds value for long polling.
Use next_token from a previous response to continue the same event list. When next_token is sent, the server does not send a new filter because Nexi keeps the previous filtering criteria.
{
"terminal_id": "your-terminal-id",
"next_token": "token-from-previous-response",
"limit": 20,
"wait_seconds": 30
}Confirmation
This MVP does not confirm by default.
When a transaction reaches AWAITING_CONFIRM, call confirm_transaction:
{
"external_id": "order-1001-payment-1",
"result_code": "SUCCESS"
}You can also let take_payment confirm automatically:
{
"external_id": "order-1002-payment-1",
"requested_amount": 500,
"auto_confirm": true
}Use auto_confirm only when your business flow is ready to accept the transaction result immediately.
SQLite storage and recovery
The server stores payment-relevant state in SQLite at NEXI_POS_STORAGE_PATH.
Stored data includes:
- external ID
- terminal ID
- transaction type
- currency
- requested amount
- state
- result code and description
- confirmation flag
- timestamps
- redacted raw transaction JSON
API credentials are never stored. Card-related fields should be redacted by the storage/client layer.
Recovery workflow after a restart:
- Call
set_terminal_idif the session terminal ID was lost and you do not useNEXI_POS_TERMINAL_ID. - Call
get_transactionwith the knownexternal_id. - Call
get_unconfirmed_transactionsfor the terminal. - If needed, call
confirm_transactionwith the correctresult_code.
SQLite improves recovery, but it is not a full production operations system. Keep your own order records and reconcile with Nexi when needed.
Troubleshooting
HTTP 401 or 403
Check NEXI_POS_API_KEY_ID and NEXI_POS_API_KEY_SECRET. Also check that the credentials are valid for the selected NEXI_POS_BASE_URL environment, for example sandbox versus production. The credentials and terminal ID are generated outside this MCP server.
Missing terminal ID
Pass terminal_id in the tool call, call set_terminal_id, or set NEXI_POS_TERMINAL_ID. There is no default terminal ID.
Amount exceeds max
The server blocks amounts above NEXI_POS_MAX_AMOUNT_MINOR. Lower requested_amount or raise NEXI_POS_MAX_AMOUNT_MINOR for your environment. Amounts are minor units, so SEK 5.00 is 500. The default 100021 allows Nexi's documented test-case trigger amounts.
Transaction stuck in PROCESSING
Do not create a new transaction immediately. Retry take_payment with the same external_id and terminal ID, or call get_transaction. If it remains in PROCESSING, check the terminal and Nexi status before deciding whether to start a new payment with a new external_id.
Transaction awaiting confirm
If state is AWAITING_CONFIRM, the card step has succeeded but the transaction is not complete. Call confirm_transaction with the same external_id and terminal ID when you are ready to accept the result. You can also use auto_confirm with take_payment for flows where immediate confirmation is correct.
Reused external_id
Use the same external_id only to retry the same transaction. If the order, amount, or payment attempt is different, use a new external_id.
Nexi simulator/test cases
Nexi documents special test-case amounts that can be used in sandbox and production simulator testing. The server detects these amounts and adds test_case information to responses. You can also trigger them by name or description with test_case instead of remembering the amount.
Examples:
test_case: "instant_success"->requested_amount: 100002test_case: "bank declined"->requested_amount: 100006requested_amount: 100017-> response includes the 10% tip test-case information
Available cases:
| Name | Amount | Category | Description |
| --- | ---: | --- | --- |
| successful_2s_delay | 100001 | success | Successful transaction with 2-second delay |
| instant_success | 100002 | success | Instant successful transaction (0 seconds) |
| rejected_by_processor | 100003 | error | Transaction rejected by payment processor |
| customer_cancelled | 100004 | error | Transaction aborted (customer cancelled) |
| card_removed | 100005 | error | Card removed during processing |
| issuer_error | 100006 | error | Issuer error (bank declined) |
| internal_system_error | 100007 | error | Internal system error |
| network_connectivity_error | 100008 | error | Network connectivity error |
| payment_method_not_accepted | 100009 | error | Card/payment method not accepted |
| contactless_pin_verified | 100010 | payment_method | Successful PIN-verified contactless transaction |
| contactless_cardholder_verification | 100011 | payment_method | Successful contactless with cardholder verification |
| chip_without_pin | 100012 | payment_method | Successful chip card without PIN |
| chip_with_pin | 100013 | payment_method | Successful chip card with PIN |
| magstripe_signature | 100014 | payment_method | Successful magnetic stripe with signature |
| magstripe_pin | 100015 | payment_method | Successful magnetic stripe with PIN |
| chip_with_pin_alternative | 100016 | payment_method | Successful chip card with PIN (alternative mode) |
| tip_10_percent | 100017 | special_feature | Successful transaction with 10% tip added |
| surcharge_1_5_percent | 100018 | special_feature | Successful transaction with 1.5% surcharge |
| dcc_enabled | 100019 | special_feature | Successful transaction with Dynamic Currency Conversion enabled |
| success_30s_delay | 100020 | success | Successful transaction with 30-second delay |
| loyalty_link_identity | 100021 | special_feature | Successful transaction with Nexi Loyalty Link customer identity in response |
Implementation notes for integration
The MCP tool layer expects these core modules to exist:
src/config.tsexportinggetConfig()with API, currency, terminal fallback, max amount, request timeout, and storage path settings.src/nexi-client.tsexportingNexiClientwithpurchase,refund,confirm,getTransaction,getUnconfirmedTransactions,getTerminalStatus, andlistTerminalEventsmethods.src/storage/sqlite-store.tsexportingSQLiteStorewithsaveIntent,updateTransaction, andmarkConfirmedmethods.
These modules are intentionally separate from the MCP tool code so the API client and storage can be tested independently.
