lunu-payment-nodejs

Lunu Payment API Client for Node.js

Usage no npm install needed!

<script type="module">
  import lunuPaymentNodejs from 'https://cdn.skypack.dev/lunu-payment-nodejs';
</script>

README

Lunu Payment. Node.js Library

Install

npm i lunu-payment-nodejs --save

API credentials

You can get your credentials in your account on the console.lunu.io website in the section https://console.lunu.io/widgets

For debugging, you can use the following credentials:

  • sandbox mode:

    • App Id: 8ce43c7a-2143-467c-b8b5-fa748c598ddd
    • API Secret: f1819284-031e-42ad-8832-87c0f1145696
  • production mode:

    • App Id: a63127be-6440-9ecd-8baf-c7d08e379dab
    • API Secret: 25615105-7be2-4c25-9b4b-2f50e86e2311

Examples

const LunuAPI = require('lunu-payment-nodejs');

const lunuAPI = new LunuAPI({
  appId: '149ec6da-f0dc-4cdf-9fb3-8ba2dc602f60',
  apiSecret: '23d93cac-000f-5000-8000-126728f15140',
});

Sandbox mode

If sandbox mode is enabled, the endpoint testing.lunu.io is used.
You can use there a test-net cryptocurrency.
To debug payment with this server, reconfigure the Lunu Wallet to test mode.

const LunuAPI = require('lunu-payment-nodejs');

const lunuAPI = new LunuAPI({
  sandbox: true,
  appId: '8ce43c7a-2143-467c-b8b5-fa748c598ddd',
  apiSecret: 'f1819284-031e-42ad-8832-87c0f1145696',
});

If sandbox mode is disabled, the endpoint alpha.lunu.io is used.

Payment creation

const LunuAPI = require('lunu-payment-nodejs');
const rfc3339 = require('./rfc3339');

const lunuAPI = new LunuAPI({
  appId: '149ec6da-f0dc-4cdf-9fb3-8ba2dc602f60',
  apiSecret: '23d93cac-000f-5000-8000-126728f15140',
});

const paymentIdempotenceKey = LunuAPI.generateIdempotencyKey();

lunuAPI.createPayment({
  /*
    customer email
    optional parameter
    Required for a refund in case of a payment error,
    including an incorrect payment amount,
    too long a transaction (more than one hour) and etc..
  */
  email: 'customer@example.com', // customer email (optional, required for refund)

  // shop order id
  shopOrderId: "208843-42-23-842",

  // order amount
  amount: '100.00',

  // amount of shipping
  amountOfShipping: "15.00",

  // Callback URL to notify your system of payment status
  callbackUrl: 'https://website.com/api/change-status',

  // Brief description of the order
  description: 'Order #1 | product1 x 12, product2 x 2',

  // Payment expiration date
  expires: rfc3339(new Date((new Date()).getTime() + 36e5)),
}, {
  // Idempotence Key (optional)
  idempotenceKey: paymentIdempotenceKey,
}).finally((error, payment) => {
  if (error) {
    console.error(error);
    return;
  }

  const {
    id, // '922a2f06-6bec-4eee-a55c-76d549f46262',
    status, // 'pending',
    amount, // '1',
    currency, // 'EUR',
    description, // 'Order #1 | product1 x 12, product2 x 2',
    confirmation_token, // '8cae7ede-944c-49b8-8a55-2a1116f70631',
    created_at, // '2021-04-06T11:11:41.528648+00:00',
    expires, // '2020-02-22T00:00:00+00:00'
  } = payment;

});

Payment widget display

The payment widget can be displayed in two ways:

  • get a link to the payment page and redirect the user to it (getPaymentWidgetUrl);
  • initialize the payment widget script directly on your website;

Getting the URL of the payment widget

const LunuAPI = require('lunu-payment-nodejs');

const lunuAPI = new LunuAPI({
  appId: '149ec6da-f0dc-4cdf-9fb3-8ba2dc602f60',
  apiSecret: '23d93cac-000f-5000-8000-126728f15140',
});

const paymentWidgetUrl = lunuAPI.getPaymentWidgetUrl({
  confirmationToken: '8cae7ede-944c-49b8-8a55-2a1116f70631',

  /*
  The page to which the service transfers the user if the payment is successful.
  */
  successUrl: 'https://website.com/success',

  /*
  The page to which the service transfers the user if the payment is canceled.
  */
  cancelUrl: 'https://website.com/cancel',
});
// => 'https://widget.lunu.io/alpha/#/?action=select&cancel=https:%2F%2Fwebsite.com%2Fcancel&success=https:%2F%2Fwebsite.com%2Fsuccess&token=922a2f06-6bec-4eee-a55c-76d549f46262'

JS code for displaying a payment widget on a website page.

The first way.
To initialize the widget, insert the following code into the body of the html page:

<script>
(function(d, t) {
  var n = d.getElementsByTagName(t)[0], s = d.createElement(t);
  s.type = 'text/javascript';
  s.charset = 'utf-8';
  s.async = true;
  // s.src = 'https://plugins.lunu.io/packages/widget-ui/testing.js?t=' + 1 * new Date(); // testing server
  s.src = 'https://plugins.lunu.io/packages/widget-ui/alpha.js?t=' + 1 * new Date(); // production server
  s.onload = function() {
    const removeWidget = window.Lunu.API.openWidget({
      /*
      Token that must be received from the Processing Service before making a payment
      Required parameter
      */
      confirmation_token: '8cae7ede-944c-49b8-8a55-2a1116f70631',

      callbacks: {
        payment_paid() {
          // Handling a successful payment event
        },
        payment_cancel() {
          // Handling a payment cancellation event
        },
        payment_close() {
          // Handling the event of closing the widget window
        },
      },
    });

    // Cancel the opening of the widget or remove the widget if it is already open.
    // removeWidget();
  };
  n.parentNode.insertBefore(s, n);
})(document, 'script');
</script>

The second way.
Use NPM package in your front-end application.

const {
  openPaymentWidgetInCurrentWindow,
} = require('lunu-payment');

const removeWidget = openPaymentWidgetInCurrentWindow({
  confirmation_token: '8cae7ede-944c-49b8-8a55-2a1116f70631',

  callbacks: {
    payment_paid: (paymentInfo) => {
      // Handling a successful payment event
    },
    payment_cancel: () => {
      // Handling a payment cancellation event
    },
    payment_close: () => {
      // Handling the event of closing the widget window
    }
  },
});

// Cancel the opening of the widget or remove the widget if it is already open.
// removeWidget();

More about this method here: https://gitlab.lunu.io/widget/lunu-payment-widget-js

Get payment by id

The store checks the validity of the notification received by requesting payment information by id from the Processing Service.

const LunuAPI = require('lunu-payment-nodejs');

const lunuAPI = new LunuAPI({
  appId: '149ec6da-f0dc-4cdf-9fb3-8ba2dc602f60',
  apiSecret: '23d93cac-000f-5000-8000-126728f15140',
});

lunuAPI
  .getPaymentById('922a2f06-6bec-4eee-a55c-76d549f46262')
  .finally((error, payment) => {
    console.log({
      error,
      payment,
    });

    const {
      id, // '922a2f06-6bec-4eee-a55c-76d549f46262',
      status, // 'pending',
      shop_order_id, // '208843-42-23-842',
      amount, // '100.00',
      currency, // 'EUR',
      description, // 'Order #1 | product1 x 12, product2 x 2',
      confirmation_token, // '8cae7ede-944c-49b8-8a55-2a1116f70631',
      created_at, // '2021-04-06T11:11:41.528648+00:00',
      expires, // '2020-02-22T00:00:00+00:00'
    } = payment;
  });

Get payment by confirmation token

const LunuAPI = require('lunu-payment-nodejs');

const lunuAPI = new LunuAPI({
  appId: '149ec6da-f0dc-4cdf-9fb3-8ba2dc602f60',
  apiSecret: '23d93cac-000f-5000-8000-126728f15140',
});

lunuAPI
  .getPaymentByToken('8cae7ede-944c-49b8-8a55-2a1116f70631')
  .finally((error, payment) => {
    console.log({
      error,
      payment,
    });
  });

Creating a refund

const LunuAPI = require('lunu-payment-nodejs');

const lunuAPI = new LunuAPI({
  appId: '149ec6da-f0dc-4cdf-9fb3-8ba2dc602f60',
  apiSecret: '23d93cac-000f-5000-8000-126728f15140',
});

const refundIdempotenceKey = LunuAPI.generateIdempotencyKey();

lunuAPI.createRefund({
  // payment id
  paymentId: '922a2f06-6bec-4eee-a55c-76d549f46262',

  // refund amount
  amount: '0.5',

  /*
   customer email
   A link will be sent to this customer email for the refund procedure.
  */
  email: 'customer@example.com',
}, {
  // Idempotence Key (optional)
  idempotenceKey: refundIdempotenceKey,
}).finally((error, refund) => {
  console.log('createRefund:', {
    error,
    refund,
  });

  const {
    purpose, // 'R-1254-1'
    iban, // 'GB29NWBK60161331926819'
    fiat_amount, // '0.5'
    amount_too_big, // false
  } = refund;

});

Lunu Payment API. General information.

URL pattern:

https://{testing|alpha}.lunu.io/api/v1/<method>

API endpoints:

  • alpha.lunu.io - production server
  • testing.lunu.io - server for product debugging in the sandbox, you can use there a test-net cryptocurrency. To debug payment with this server, reconfigure the Lunu Wallet to test mode

The API is available for authorized users. Unauthorized users receive an empty response and status

404 Not found

All responses are returned in JSON format.

The responses from the server are wrapped:

  • a successful response is returned in the response field:
{
   "response": {...}
}
  • if it is necessary to return an error, then the error is returned in the error field, for example:
{
   "error": {
     "code": 1,
     "message": "..."
   }
}

Authentication

HTTP Basic Auth must be used to authenticate requests. For the request headers, you must enter the merchant ID as the username, and the secret key as the password.

Example header:

Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l

where QWxhZGRpbjpPcGVuU2VzYW1l is the result of the function: base64(app_id + ':' + secret_key)

Idempotency

From the API's point of view, idempotency means that multiple requests are handled in the same way as single requests.
It means that upon receiving a repeated request with the same parameters, the Processing Service will return the result of the original request in response.
This approach helps to avoid the unwanted replay of transactions. For example, if during a payment there are network problems and the connection is interrupted, you can safely repeat the required request as many times as you need.
GET requests are idempotent by default, since they have no unwanted consequences.
To ensure the idempotency of POST requests, the Idempotence-Key header (or idempotence key) is used.

Example header:

Idempotence-Key: 3134353

where 3134353 is the result of the function: uniqid()

The idempotency key needs to be unique within the individual application ID of the account.
One application ID cannot be used in several stores, otherwise it may not be sufficient to use only the store's internal order number as the idempotency key, since these values may be repeated in requests from other stores with the same application ID.

Scenario for making a payment through the Widget

When the user proceeds to checkout (this can be either a single product or a basket of products), the payment process goes through the following stages:

1. Payment creation. payments/create

The merchant's website or application sends a request to the Processing Service to create a payment, which looks like this:

POST https://alpha.lunu.io/api/v1/payments/create
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l
Idempotence-Key: 3134353
Content-Type: application/json
{
  "email": "customer@example.com",
  "shop_order_id": "208843-42-23-842",
  "amount": "100.00",
  "amount_of_shipping": "15.00",
  "callback_url": "https://website.com/api/change-status",
  "description": "Order #208843-42-23-842",
  "expires": "2020-02-22T00:00:00-00:00"
}

Description of fields:

  • email (string) (optional parameter) - customer email; used when a refund is required;

  • shop_order_id (string) (optional parameter) - shop order id;

  • amount (string) - payment amount (currency is indicated in the merchant's profile);

  • amount_of_shipping (string) (optional parameter) - amount of shipping;

  • callback_url (string) (optional parameter) - url-address of the store's callback API, to which the Processing service will send a request when the payment status changes (when the payment is made)

  • description (string) (optional parameter) - if you need to add a description of the payment that the seller wants to see in its personal account, then you need to pass the description parameter. The description should be no more than 128 characters.

  • expires (string) (optional parameter) - date when the payment expires, in RFC3339 format. By default: 1 minute from the moment of sending;

The Processing Service returns the created payment object with a token for initializing the widget.

{
  "id": "23d93cac-000f-5000-8000-126628f15141",
  "status": "pending",
  "amount": "100.00",
  "currency": "EUR",
  "description": "Order #208843-42-23-842",
  "confirmation_token": "ct-24301ae5-000f-5000-9000-13f5f1c2f8e0",
  "created_at": "2019-01-22T14:30:45-03:00",
  "expires": "2020-02-22T00:00:00-00:00"
}

Description of fields:

  • id (string) - payment ID;

  • status (string) - payment status. Value options:

    • "pending" - awaiting payment;
    • "awaiting_payment_confirmation" - the transaction was found in the mempool, it is awaiting confirmation in the blockchain network;
    • "paid" - payment has been made;
    • "canceled" - the payment was canceled by the seller;
    • "expired" - the time allotted for the payment has expired;
  • amount (string)- amount of payment;

  • currency (string) - payment currency;

  • description (string) - payment description, no more than 128 characters;

  • confirmation_token (string) - payment token, which is required to initialize the widget;

  • created_at (string) - the date the payment was created;

  • expires (string) - the date when the payment expires, in RFC3339 format.

2. Initialize the widget.

const {
  openPaymentWidgetInNewWindow,
} = require('lunu-payment');


openPaymentWidgetInNewWindow({
  confirmationToken: '5bd68fb4-70ed-4b0d-b470-b20bc6773f7d',
  successUrl: 'https://example.com/payment-success',
  cancelUrl: 'https://example.com/payment-cancel',
});

3. Notifying the seller's store about a change in payment status. Payment Callback

When the user has made a payment, the Processing Service sends a request in the following format to the store's API url, which was specified at the time of creating the payment:

POST https://website.com/api/change-status
{
  "id": "23d93cac-000f-5000-8000-126628f15141",
  "shop_order_id": "208843-42-23-842",
  "status": "paid",
  "amount": "100.00",
  "currency": "EUR",
  "description": "Order #208843-42-23-842",
  "created_at": "2019-01-22T14:30:45-03:00",
  "expires": "2020-02-22T00:00:00-00:00"
}

Description of fields:

  • id (string) - payment ID;

  • status (string) - payment status. Value options:

    • "awaiting_payment_confirmation" - the transaction was found in the mempool, it is awaiting confirmation in the blockchain network;
    • "paid" - payment has been made;
    • "canceled" - the payment was canceled by the seller;
    • "expired" - the time allotted for the payment has expired;
  • shop_order_id (string) (optional parameter) - shop order id;

  • amount (string)- amount of payment;

  • currency (string) - payment currency;

  • description (string) - payment description, no more than 128 characters;

  • created_at (string) - the date the payment was created;

  • expires (string) - the date when the payment expires, in RFC3339 format.

4. The store checks the validity of the notification received. payments/get/{payment_id}

After the merchant has received a notification about the change in the payment status, he needs to check the validity of this notification through the Processing Service by the following request:

POST https://alpha.lunu.io/api/v1/payments/get/{payment_id}
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l

If everything is good then the Processing Service returns an identical payment object:

{
  "id": "23d93cac-000f-5000-8000-126628f15141",
  "status": "paid",
  "shop_order_id": "208843-42-23-842",
  "amount": "100.00",
  "currency": "EUR",
  "description": "Order #208843-42-23-842",
  "created_at": "2019-01-22T14:30:45-03:00",
  "expires": "2020-02-22T00:00:00-00:00"
}

Refunds API

Example of creating a refund:

POST https://alpha.lunu.io/api/v1/refund/create
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l
Idempotence-Key: ps_refund_1614589640890_3134
Content-Type: application/json
{
  "payment_id": "23d93cac-000f-5000-8000-126628f15141",
  "value_fiat": "50.5",
  "email": "customer_email@example.com"
}

Description of fields:

  • payment_id (string) - payment ID;
  • value_fiat (string) - refund amount in fiat;
  • email (string) - customer email; A link will be sent to this address for the refund procedure.

Response if successful:

{
  "response": {
    "purpose": "R-1254-1",
    "iban": "GB29NWBK60161331926819",
    "fiat_amount": "21.00",
    "amount_too_big": false
  }
}

Description of fields:

  • purpose (string) - payment purpose (R-{order number}-{refund number}, for example R-1254-1);
  • iban (string) - IBAN;
  • value_fiat (string) - refund amount in fiat;
  • amount_too_big (boolean) - a flag signaling that the return has been partially executed from the amount that was set in the parameters. The flag is true when the refund amount for the payment exceeds the payment amount.

Response if failure:

{
  "error": {
    "code": 404,
    "message": "Payment not found"
  }
}