As of 24.11.2024, this repository will be in archived state and is no longer developed or maintained.
npm install banking-ws
This is javascript library for financial messages using web services mainly targeted for Node.js
This library can:
- Create certificates for Samlink banking web service.
- It can construct and validate SOAP messages.
- Get and renew certificates.
- Make payment files and ask payment statuses which are represented as json.
- Request and parse bank statements and represented them as json.
- Banks supported by Samlink.
- http://xsd2xml.com/ is handy for converting
xsd
schemas toxml
samples. - Sepa XML validation is handy for
validating SEPA
pain
messages. - Chilkat - xmlDsigVerify lifesaver with XML signature verification. The only tool which gave confidence.
- https://samlink.fi/ohjelmistopalvelut/
- https://samlink.fi/app/uploads/2023/05/Palvelukuvaus_C2B_Pain_03.pdf
- https://www.mobilefish.com/services/sepa_xml_validation/sepa_xml_validation.php
- https://www.finanssiala.fi/wp-content/uploads/2021/03/WebServices_Messages_v110_20200504.pdf
- https://www.finanssiala.fi/en/topics/payment-services-in-finland/payment-technical-documents/
- https://www.finanssiala.fi/wp-content/uploads/2021/03/ISO20022_Payment_Guide.pdf
- https://samlink.fi/app/uploads/2023/05/XML-account_statement_camt.053.001.02.pdf
Special thanks for contributing:
- Check that dependencies are up-to-date
->
latest. npm run test
andnpm run lint
gives no errors.- Increment library version from
package.json
file.
- Mac and Linux already has OpenSSL pre-installed but with Windows you need to download binary somewhere like here.
- Then add its binary to your system path variable
;C:\Program Files\OpenSSL-Win64\bin
-
Install npm package via:
npm install banking-ws
or
yarn add banking-ws
-
In case of developing / testing this library make sure that global ts-node is at latest version
npm install -g ts-node@latest
Running as docker container could be done in following way:
FROM node:18.14.2-alpine
RUN apk add openssl
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD [ "node", "./src/payment-application.js"]
this sample assumes that you have payment application having banking-ws
as dependency.
OpenSSL is added here since banking-ws
requires its features to create keys.
SAMLINK_TEST_ROOT_CA
is used with axios ca config at Samlink test side. Taken from Samlink documentation provided with customerId.BANK_CERTIFICATE
is a base64 encoded certificate get after initial GetCertificate command and new ones after RenewCertificate.CLIENT_CERTIFICATE
is signing certificate you have created with OpenSSL.CLIENT_PRIVATE_KEY
is private key for upper signing certificate you created with OpenSSL
You need certificates from your bank which is got with get certificate request. To make request you first need:
- Your own generated signing certificate (
CLIENT_CERTIFICATE
). - Private key for your signing certificate (
CLIENT_PRIVATE_KEY
).
Then you need to get your bank to sign your signing/encryption certificate(s). For this you need to make
- Your signing certificate signing request
You can generate your certificate signing requests with openssl
openssl req -out signing.csr -new -newkey rsa:2048 -nodes -keyout signing.key
After this, you should have the following files:
signing.csr (CLIENT_CERTIFICATE)
signing.key (CLIENT_PRIVATE_KEY)
- You cannot use this without Samlink opening firewall rule to allow you making request behind your public ip.
- You need to ask Samlink for customer id and one time use transfer key.
Below are examples for each step.
✅ Production tested
Note that customerId
must be exact Samlink customer id.
import {CreatedCertificateInterface} from './src/interfaces';
import {CreateOwnCertificate} from './src/index';
CreateOwnCertificate({
twoLetterCountryCode: 'FI',
stateOrProvince: 'Province',
city: 'City',
companyName: 'Company name',
companyUnitName: 'Company unit',
customerId: '12345678',
emailAddress: '[email protected]',
}).then((certificate: CreatedCertificateInterface) => {
console.log(certificate);
});
{
clientCertificate: 'base64-encoded-content',
clientPrivateKey: 'base64-encoded-content'
}
You can decode clientCertificate
and then its pem base64 and then see that your given parameters exists.
✅ Production tested
import * as moment from 'moment'
import {GetCertificateInterface, SoftwareIdInterface, UserParamsInterface} from './src/interfaces';
import {GetCertificate} from './src/index';
const SAMLINK_TEST_ROOT_CA = "base64-content-here"; // in production use value: null
const CLIENT_CERTIFICATE = "base64-content-here";
const userParams: UserParamsInterface = {
bank: 'Samlink',
environment: 'PRODUCTION',
customerId: '12345678',
Base64EncodedRootCA: SAMLINK_TEST_ROOT_CA,
rejectUnauthorized: true,
};
const gc: GetCertificateInterface = {
userParams: userParams,
requestUrl: 'https://185.251.49.57/wsdl/CertificateService.xml',
Timestamp: new moment().format('YYYY-MM-DDThh:mm:ssZ'),
SoftwareId: {name: 'TEST', version: '0.9.0'} as SoftwareIdInterface,
Command: 'GetCertificate',
Service: 'ISSUER',
Base64EncodedClientCsr: CLIENT_CERTIFICATE,
TransferKey: '123123123123123123123',
RequestId: '123456'
};
const certificate = await GetCertificate(gc);
console.log(certificate);
This certificate is later used with BANK_CERTIFICATE
variable.
{
Name: 'SURNAME=<your-customer-id>, CN=<your-company-name>, O=<>, C=<country>',
Certificate: 'base64-encoded-certificate',
CertificateFormat: 'X509',
ExpirationDateTime: '<certificate-expiration-date>'
}
Certificate should be renewed with Renew Certificate method before it's expired.
You can see X509v3 certificate details after Base64 decoding response certificate and then using OpenSSL to view it:
openssl x509 -in certificate_file_name.extension -text
Note that renew request is made using current still active BANK_CERTIFICATE
and your
old CLIENT_PRIVATE_KEY
. From a new key pair you pass to this call only new CLIENT_CERTIFICATE
.
You should use this Generate new certificate to make new ones.
I have named below variables in "new" and "old" ways which explains what to use and where.
import * as moment from 'moment';
import {GetCertificateInterface, SoftwareIdInterface, UserParamsInterface} from './src/interfaces';
import {RenewCertificate} from './src/index';
const SAMLINK_TEST_ROOT_CA = "base64-content-here"; // in production use value: null
const BANK_CERTIFICATE_OLD = "base64-content-here";
const CLIENT_CERTIFICATE_NEW = "base64-content-here";
const CLIENT_PRIVATE_KEY_OLD = "base64-content-here";
const userParams: UserParamsInterface = {
bank: 'Samlink',
environment: 'PRODUCTION',
customerId: '12345678',
Base64EncodedRootCA: SAMLINK_TEST_ROOT_CA,
rejectUnauthorized: true,
};
const gc: GetCertificateInterface = {
userParams: userParams,
requestUrl: 'https://185.251.49.57/wsdl/CertificateService.xml',
Timestamp: moment().format('YYYY-MM-DDThh:mm:ssZ'),
SoftwareId: {name: 'TEST', version: '0.9.0'} as SoftwareIdInterface,
Command: 'RenewCertificate',
Base64EncodedClientCsr: CLIENT_CERTIFICATE_NEW,
Base64EncodedBankCsr: BANK_CERTIFICATE_OLD,
RequestId: '123456',
Base64EncodedClientPrivateKey: CLIENT_PRIVATE_KEY_OLD,
};
const certificate = await RenewCertificate(gc);
console.log(certificate);
{
Name: 'SURNAME=<your-customer-id>, CN=<your-company-name>, O=<>, C=<country>',
Certificate: 'base64-encoded-certificate',
CertificateFormat: 'X509',
ExpirationDateTime: '<certificate-expiration-date>'
}
Certificate should be renewed with Renew Certificate method before it's expired.
✅ Production tested
Known as camt.053.001.02
.
import * as moment from 'moment';
import {XTInterface, SoftwareIdInterface, UserParamsInterface} from './src/interfaces';
import {BankStatement} from './src/index';
const SAMLINK_TEST_ROOT_CA = "base64-content-here"; // in production use value: null
const BANK_CERTIFICATE = "base64-content-here";
const CLIENT_CERTIFICATE = "base64-content-here";
const CLIENT_PRIVATE_KEY = "base64-content-here";
const userParams: UserParamsInterface = {
bank: 'Samlink',
environment: 'PRODUCTION',
customerId: '12345678',
Base64EncodedRootCA: SAMLINK_TEST_ROOT_CA,
rejectUnauthorized: true,
};
const xt: XTInterface = {
userParams: userParams,
verifyResponseSignature: true,
requestUrl: 'https://185.251.49.57/services/CorporateFileService',
RequestId: '123456',
Timestamp: moment().format('YYYY-MM-DDThh:mm:ssZ'),
SoftwareId: {name: 'TEST', version: '0.9.0'} as SoftwareIdInterface,
ExecutionSerial: '123456',
Base64EncodedClientCsr: CLIENT_CERTIFICATE,
Base64EncodedBankCsr: BANK_CERTIFICATE,
Base64EncodedClientPrivateKey: CLIENT_PRIVATE_KEY,
language: 'FI',
};
const bankStatement = await BankStatement(xt);
console.log(bankStatement);
Below is sample of retrieved bank statement details:
{
FileReference: '1536794526',
TargetId: 'NONE',
UserFilename: 'STOL001.OLTCX60H.CAMT053.PS',
FileType: 'XT',
FileTimestamp: '2023-06-30T21:23:44.678+03:00',
Status: 'NEW',
ForwardedTimestamp: '2023-06-30T21:23:44.678+03:00',
Deletable: 'false'
}
After bank statement response, use download file option to download bank statement file using FileReference
retrieved
with this method.
Below is just a mock sample leading to invalid validation result.
import {SEPAPaymentInformationInterface, XLPaymentInfoValidationInterface} from './interfaces';
import {SEPAPaymentInfoValidation} from './index';
const pmtInf: SEPAPaymentInformationInterface[] = [{
PmtInfId: '20210815-12345678912',
PmtMtd: 'TRF',
PmtTpInf: {
SvcLvl: {
Cd: 'BANK',
}
},
ReqdExctnDt: '2021-08-15',
Dbtr: {
Nm: 'Origin Company',
PstlAdr: {
Ctry: 'FI',
AdrLine: 'Test street 123',
AdrLine2: 'FI-00100 Helsinki',
},
Id: {
OrgId: {
Othr: {
Id: '12345678913',
SchmeNm: {
Cd: 'BANK',
}
}
}
}
},
DbtrAcct: {
Id: {
IBAN: 'FI1234567891234567',
}
},
DbtrAgt: {
FinInstnId: {
BIC: 'ITELFIHH',
}
},
ChrgBr: 'SLEV',
CdtTrfTxInf: {
PmtId: {
InstrId: 'InstrId000002',
EndToEndId: 'EndToEndId000001',
},
PmtTpInf: {
SvcLvl: {
Cd: 'BANK',
}
},
Amt: {
InstdAmt: '425.60',
},
ChrgBr: 'SLEV',
CdtrAgt: {
FinInstnId: {
BIC: 'ITELFIHH',
}
},
Cdtr: {
Nm: 'Hello World',
PstlAdr: {
Ctry: 'FI',
AdrLine: 'Test street 321',
AdrLine2: 'FI-00200 Helsinki',
},
Id: {
OrgId: {
Othr: {
Id: '12345678914',
SchmeNm: {
Cd: 'BANK',
}
}
}
}
},
CdtrAcct: {
Id: {
IBAN: 'FI1234567891234568',
}
},
RmtInf: {
Ustrd: 'Sample invoice 123',
}
}
}];
const xlPmtInfo: XLPaymentInfoValidationInterface = {PmtInf: pmtInf}
const sepaPaymentValidation = await SEPAPaymentInfoValidation(xlPmtInfo);
console.log(sepaPaymentValidation);
This example is invalid and errors contains list of issues
{
PmtInf: [
{
valid: false,
errors: [
{
code: 6,
status: 'WrongAccountBankBranchChecksum'
},
{
code: 5,
status: 'WrongIBANChecksum'
}
]
}
]
}
✅ Production tested
Based on https://samlink.fi/app/uploads/2023/05/Palvelukuvaus_C2B_Pain_03.pdf
Where request is called Pain001.001.03
and returned status response Pain002.001.03
.
Given details are not validated. Use earlier SEPA payment validation method to validate your payment details.
Current limitation for this library is that payment information C part =
Credit Transfer Transaction Information
can hold only one transaction. This means that
NbOfTxs
will be same as PmtInf
array count.
import * as moment from 'moment';
import {XLInterface, SoftwareIdInterface, UserParamsInterface} from './src/interfaces';
import {SEPAPayment} from './src/index';
const SAMLINK_TEST_ROOT_CA = "base64-content-here"; // in production use value: null
const BANK_CERTIFICATE = "base64-content-here";
const CLIENT_CERTIFICATE = "base64-content-here";
const CLIENT_PRIVATE_KEY = "base64-content-here";
const userParams: UserParamsInterface = {
bank: 'Samlink',
environment: 'PRODUCTION',
customerId: '12345678',
Base64EncodedRootCA: SAMLINK_TEST_ROOT_CA,
rejectUnauthorized: true,
};
const xl: XLInterface = {
userParams: userParams,
requestUrl: 'https://185.251.49.57/services/CorporateFileService',
RequestId: '123456',
Timestamp: moment().format('YYYY-MM-DDThh:mm:ssZ'),
SoftwareId: {name: 'TEST', version: '0.9.0'} as SoftwareIdInterface,
ExecutionSerial: '123456',
Base64EncodedClientCsr: CLIENT_CERTIFICATE,
Base64EncodedBankCsr: BANK_CERTIFICATE,
Base64EncodedClientPrivateKey: CLIENT_PRIVATE_KEY,
language: 'FI',
sepa: {
CcyOfTrf: 'EUR',
GrpHdr: {
MsgId: 'MSGID000001',
CreDtTm: moment().format('YYYY-MM-DDThh:mm:ssZ'),
InitgPty: {
Nm: 'Origin Company',
PstlAdr: {
Ctry: 'FI',
AdrLine: 'Test street 123',
AdrLine2: 'FI-00100 Helsinki',
},
Id: {
OrgId: {
Othr: {
Id: '1234567890',
SchmeNm: {
Cd: 'BANK',
}
}
}
}
}
},
PmtInf: [{
PmtInfId: '20210815-12345678912',
PmtMtd: 'TRF',
PmtTpInf: {
SvcLvl: {
Cd: 'BANK',
}
},
ReqdExctnDt: '2021-08-15',
Dbtr: {
Nm: 'Origin Company',
PstlAdr: {
Ctry: 'FI',
AdrLine: 'Test street 123',
AdrLine2: 'FI-00100 Helsinki',
},
Id: {
OrgId: {
Othr: {
Id: '12345678912',
SchmeNm: {
Cd: 'BANK',
}
}
}
}
},
DbtrAcct: {
Id: {
IBAN: 'FI1234567891234567',
}
},
DbtrAgt: {
FinInstnId: {
BIC: 'ITELFIHH',
}
},
ChrgBr: 'SLEV',
CdtTrfTxInf: {
PmtId: {
InstrId: 'InstrId000001',
EndToEndId: 'EndToEndId000001',
},
PmtTpInf: {
SvcLvl: {
Cd: 'BANK',
}
},
Amt: {
InstdAmt: '500.00',
},
ChrgBr: 'SLEV',
CdtrAgt: {
FinInstnId: {
BIC: 'ITELFIHH',
}
},
Cdtr: {
Nm: 'Testi Testinen',
PstlAdr: {
Ctry: 'FI',
AdrLine: 'Test street 321',
AdrLine2: 'FI-00200 Helsinki',
},
Id: {
OrgId: {
Othr: {
Id: '12345678913',
SchmeNm: {
Cd: 'BANK',
}
}
}
}
},
CdtrAcct: {
Id: {
IBAN: 'FI1234567891234568',
}
},
RmtInf: {
Ustrd: 'Sample invoice 123',
}
}
}]
},
logResponse: false,
};
const sepaPayment = await SEPAPayment(xl);
console.log(sepaPayment);
Is object of FileDescriptor.
{
FileReference: '530253',
TargetId: 'NONE',
FileType: 'XL',
FileTimestamp: '2021-08-15T21:29:48.497+03:00',
Status: 'WFP',
AmountTotal: '500.0',
TransactionCount: '1',
Deletable: 'false'
}
WFP = Waiting for processing
WFC = Waiting for confirmation
FWD = Forwarded to processing
DLD = Downloaded
DEL = Deleted
NEW = New file
KIN = Key-in
✅ Production tested
Returns list of SEPA-XML descriptors. Known as Pain002.001.03
related to earlier section payments.
Use fileStatus
to filter what descriptors are looked for.
import * as moment from 'moment';
import {XPInterface, SoftwareIdInterface, UserParamsInterface} from './src/interfaces';
import {DownloadFileList} from './src/index';
const SAMLINK_TEST_ROOT_CA = "base64-content-here"; // in production use value: null
const BANK_CERTIFICATE = "base64-content-here";
const CLIENT_CERTIFICATE = "base64-content-here";
const CLIENT_PRIVATE_KEY = "base64-content-here";
const userParams: UserParamsInterface = {
bank: 'Samlink',
environment: 'PRODUCTION',
customerId: '12345678',
Base64EncodedRootCA: SAMLINK_TEST_ROOT_CA,
rejectUnauthorized: true,
};
const xp: XPInterface = {
userParams: userParams,
verifyResponseSignature: true,
requestUrl: 'https://185.251.49.57/services/CorporateFileService',
RequestId: '123456',
Timestamp: moment().format('YYYY-MM-DDThh:mm:ssZ'),
SoftwareId: {name: 'TEST', version: '0.9.0'} as SoftwareIdInterface,
ExecutionSerial: '123456',
Base64EncodedClientCsr: CLIENT_CERTIFICATE,
Base64EncodedBankCsr: BANK_CERTIFICATE,
Base64EncodedClientPrivateKey: CLIENT_PRIVATE_KEY,
language: 'FI',
fileStatus: 'NEW',
};
const xpFileList = await DownloadFileList(xp);
console.log(xpFileList);
Following array sample is example when you have two SEPA payments sent to bank system.
[
{
FileReference: '530259',
TargetId: 'NONE',
UserFilename: 'NONE.palaute',
ParentFileReference: '530258',
FileType: 'XP',
FileTimestamp: '2021-08-17T20:47:39.775+03:00',
Status: 'NEW',
ForwardedTimestamp: '2021-08-17T20:47:39.775+03:00',
Deletable: 'false'
},
{
FileReference: '530254',
TargetId: 'NONE',
UserFilename: 'NONE.palaute',
ParentFileReference: '530253',
FileType: 'XP',
FileTimestamp: '2021-08-15T21:29:48.349+03:00',
Status: 'NEW',
ForwardedTimestamp: '2021-08-15T21:29:48.349+03:00',
Deletable: 'false'
}
]
Return is always an array. Either empty or populated like in sample.
ParentFileReference
is our side SEPA message used reference.
✅ Production tested
Allows downloading files using file reference id. Response parsed content is based on used file type. Supported file types are:
XP
= retrieval of sepa payment status for pain.002.001.02 ja .03
XT
= retrieval of bank statement for camt.053.001.02
import * as moment from 'moment';
import {DFInterface, SoftwareIdInterface, UserParamsInterface} from './src/interfaces';
import {DownloadFile} from './src/index';
const SAMLINK_TEST_ROOT_CA = "base64-content-here"; // in production use value: null
const BANK_CERTIFICATE = "base64-content-here";
const CLIENT_CERTIFICATE = "base64-content-here";
const CLIENT_PRIVATE_KEY = "base64-content-here";
const userParams: UserParamsInterface = {
bank: 'Samlink',
environment: 'PRODUCTION',
customerId: '12345678',
Base64EncodedRootCA: SAMLINK_TEST_ROOT_CA,
rejectUnauthorized: true,
};
const df: DFInterface = {
userParams: userParams,
verifyResponseSignature: true,
requestUrl: 'https://185.251.49.57/services/CorporateFileService',
RequestId: '123456',
Timestamp: moment().format('YYYY-MM-DDThh:mm:ssZ'),
SoftwareId: {name: 'TEST', version: '0.10.0'} as SoftwareIdInterface,
ExecutionSerial: '123456',
Base64EncodedClientCsr: CLIENT_CERTIFICATE,
Base64EncodedBankCsr: BANK_CERTIFICATE,
Base64EncodedClientPrivateKey: CLIENT_PRIVATE_KEY,
language: 'FI',
fileType: 'XP',
fileReferences: ['553481']
};
const fileDescriptors = await DownloadFile(df);
console.log(fileDescriptors);
Following is a sample response for requested XP
file reference:
{
"FileReference": "553481",
"TargetId": "NONE",
"UserFilename": "NONE.palaute",
"ParentFileReference": "553480",
"FileType": "XP",
"FileTimestamp": "2023-03-09T18:59:10.193+02:00",
"Status": "DLD",
"ForwardedTimestamp": "2023-03-09T18:59:10.193+02:00",
"Deletable": "false",
"PaymentStatusReport": {
"CreateDateTime": "2023-03-09T16:59:10.183Z",
"MessageIdentifier": "01871490_1678381150183",
"OriginalMessageIdentification": "MSGID202303",
"OriginalPaymentInformationIdentification": "pain.001.001.02",
"Status": {
"GroupStatus": "ACTC",
"TransactionStatus": "-",
"StatusReasonInformationDescriptions": [
"No Description."
],
"StatusReasonInformationCode": "-",
"AdditionalInformation": "-"
}
},
"BankStatement": null,
}
Sample response for bank stement XT
file reference:
TODO
{
}