Making payments with cryptocurrency using PayKassa aggregator

  • Tutorial

Hello, Habr! In my last article, I talked about how to connect the cryptocurrency reception with my own hands without using third-party services. In this article I will tell you how to accept payments with cryptocurrency without raising a full node and without the associated difficulties.


But, as it always happens, you have to pay for convenience. PayKassa is , since it allows you to accept payments anonymously with average market commissions, without the need to confirm your identity and company information.


Registration and setup


Registration is very simple, with setup a little more complicated.


I advise you to create 2 stores, one for development and testing, and the second - "combat". For local development, it is very convenient to use ngrok and specify the appropriate domain (after checking in advance that it is free).


Creating a store is as simple as possible:



In the opened form, most fields should not cause difficulties:



For a test store, it will be most convenient to specify any free ngrok domain. "Handler URL" is the address where PayKassa will knock to notify that the order status has changed. "Successful / unsuccessful payment URL" - addresses to which the user will be directed after making the payment. They can place a simple page with a description of what the user needs to do after a successful / unsuccessful payment.


After creating, we need the store ID:



Further, if desired, you can specify in the settings who pays the commission and which payment systems you want to connect.


This is all that is required to start accepting payments.


Getting URL for payment


The article will use true for the test parameter in all queries. To go into combat mode, it’s enough not to specify it.


PayKassa has a simple PHP wrapper for working with the API , but for other languages, writing such a wrapper should also not be difficult. And for a better understanding of the process, all examples will be accompanied by example queries using curl.


I did not find the possibility of using ready-made pages with a choice of payment method, but this is not always required. Moreover, making it yourself is quite simple. On the page for developers there is a correspondence between payment systems and internal IDs:



We will use Bitcoin everywhere as the currency (system: 11, currency: "BTC").


The PayKassa payment API is located at the address https://paykassa.pro/sci/0.3/index.php (at the time of writing) and supports two methods: sci_create_order to get the payment URL and sci_confirm_order to check the status of the payment.


An example of obtaining a URL for payment using curl:


curl https://paykassa.pro/sci/0.3/index.php -d 'func=sci_create_order&amount=0.1&currency=BTC&order_id=1&comment=test&system=4&sci_id=SCI_ID&sci_key=SCI_KEY&domain=DOMAIN&test=false' -H 'Content-type: application/x-www-form-urlencoded'

Using the PHP library ( link to paykassa_sci.class.php ):


<?php
    require_once('paykassa_sci.class.php');

    $paykassa = new PayKassaSCI( 
        $paykassa_shop_id,       // идентификатор магазина
        $paykassa_shop_password  // пароль магазина
    );

    // параметры: сумма, валюта, номер заказа, комментарий, платежная система
    $res = $paykassa->sci_create_order(0.01, "BTC", 1, "Test Payment", 11);

    if ($res['error']) {
        echo $res['message'];   // $res['message'] - текст сообщения об ошибке
        //действия в случае ошибки
    } else {
        //  перенаправление на страницу оплаты
        $redirect_uri = $res['data']['url'];
        header("Refresh: 0; url=$redirect_uri");
    }
?>

To work with the API in other languages, it will be convenient to write a small wrapper and use it:


For ruby:


# paykassa.rb
require 'net/http'

class Paykassa
  BASE_SCI_URI = URI('https://paykassa.pro/sci/0.3/index.php')

  # вариант для Ruby On Rails свежих версий
  # в secrets.yml необходимо наличие следующих значений:
  # paykassa:
  #   sci_id: SCI_ID
  #   sci_key: SCI_SECRET_KEY
  #   domain: SCI_DOMAIN
  # если у вас не RoR - то можно просто не указывать стандартное значение:
  # def initialize(auth)
  # но в этом случае понадобится передать в auth хеш с ключами sci_id, sci_key и domain
  def initialize(auth = Rails.application.secrets[:paykassa])
    @_auth = auth
  end

  # создание запроса на оплату, в ответе получаем хеш на базе JSON пришеднего с сервера
  def create_order(amount:, currency:, order_id:, comment:, system:)
    make_request(
      func: :sci_create_order,
      amount: amount,
      currency: currency,
      order_id: order_id,
      comment: comment,
      system: system
    )
  end

  # проверка статуса платежа
  def confirm_order(private_hash)
    make_request(func: :sci_confirm_order, private_hash: private_hash)
  end

  private

  def make_request(data)
    res = Net::HTTP.post_form(BASE_SCI_URI, data.merge(@_auth))
    JSON.parse(res.body).deep_symbolize_keys
  end
end

Receiving url for payment:


    paykassa = Paykassa.new
    # если нет secrets.yml или хотим указать явно данные авторизации:
    # paykassa = Paykassa.new(sci_id: 0, sci_key: '123', domain: 'habrashop.ngrok.io')
    result = paykassa.create_order(
      amount: 0.01,
      currency: 'BTC',
      order_id: 1,
      comment: "Payment №1",
      system: 11
    )

    # если произошла ошибка
    raise StandardError.new(result[:message]) if result[:error]

    url = result[:data][:url]
    # дальше можно перенаправить пользователя по этому url: redirect_to url

For node.js:


var https = require('https');
var querystring = require('querystring');

function mergeArray(array1,array2) {
  for(item in array1) {
    array2[item] = array1[item];
  }
  return array2;
}

function PayKassaApi(sci_id, sci_key, domain, test) {
  this.sci_id = sci_id;
  this.sci_key = sci_key;
  this.domain = domain;
  this.test = test || false;
};
PayKassaApi.methods = [
  'sci_create_order',
  'sci_confirm_order'
]
PayKassaApi.prototype.sendRequest = function(method, params, callback) {
  if (PayKassaApi.methods.indexOf(method) === -1) {
    throw new Error('wrong method name ' + method)
  };

  if (callback == null) {
    callback = params;
  };

  var data = {
    method: method,
    sci_id: this.sci_id,
    sci_key: this.sci_key,
    domain: this.domain,
    test: this.test
  }
  data = mergeArray(params, data)

  var body = querystring.stringify(data);

  var options = {
    host: 'paykassa.pro',
    port: 443,
    path: '/sci/0.3/index.php',
    method: 'POST',            
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
  };

  var request = https.request(options, function (response) {
    var result = '';
    response.setEncoding('utf8');

    response.on('data', function (chunk) {
      result += chunk;
    });

    // Listener for intializing callback after receiving complete response
    response.on('end', function () {
      try {
        callback(JSON.parse(result));
      } catch (e) {
        console.error(e);
        callback(result);
      }
    });
  });

  request.write(body)
  request.end()
};

for (var i = 0; i < PayKassaApi.methods.length; i++) {
  PayKassaApi.prototype[PayKassaApi.methods[i]] = function (method) {
    return function (params, callback) {
      this.sendRequest(method, params, callback)
    }
  }(PayKassaApi.methods[i])
}

module.exports = PayKassaApi

Using:


var PayKassa = require("./paykassa")
var paykassa = new Api({ sci_id: 0, sci_key: '123', domain: 'habratest.ngrok.io', test: true })
paykassa.sci_create_order({
  amount: 0.01,
  currency: 'BTC',
  order_id: 1,
  comment: 'test order №1',
  system: 1
}, function (res) {
  if (res.error) {
    // если ошибка - кидаем Exception
    throw new Error(res.message)
  } else {
    // иначе редиректим или передаём на фронт / в темплейт
    console.log(res.data.url)
  }
})

For python:


import httplib
import urllib
import json

class PayKassa:
    sci_domain = 'paykassa.pro'
    sci_path = '/sci/0.3/index.php'

    def __init__(self, sci_id, sci_key, domain, test):
        self.sci_id = sci_id
        self.sci_key = sci_key
        self.domain = domain
        self.test = test and 'true' or 'false'

    def sci_create_order(self, amount, currency, order_id, comment, system):
        return self.make_request({
            'func': 'sci_create_order',
            'amount': amount,
            'currency': currency,
            'order_id': order_id,
            'comment': comment,
            'system': system
        })

    def sci_confirm_order(self, private_hash):
        return self.make_request({ 'func': 'sci_confirm_order', 'private_hash': private_hash })

    def make_request(self, params):
        fields = {'sci_id': self.sci_id, 'sci_key': self.sci_key, 'domain': self.domain, 'test': self.test}.copy()
        fields.update(params)
        encoded_fields = urllib.urlencode(fields)
        headers = {'Content-Type': 'application/x-www-form-urlencoded'}

        conn = httplib.HTTPSConnection(self.sci_domain)
        conn.request('POST', self.sci_path, encoded_fields, headers)
        response = conn.getresponse()
        return json.loads(response.read())

Using:


paykassa = PayKassa(0, '123', 'habratest.ngrok.io', False)
result = paykassa.sci_create_order(0.001, 'BTC', 1, 'Order number 1', 11)
if result['error']:
  print(result['message'])
else:
  print(result['data']['url'])

The request itself is a regular multipart form / data POST request.


Description of parameters:


  • func - API method
  • system - ID of the payment system, a list of matching IDs and payment systems, see above
  • currency - payment currency, possible values ​​also see above
  • amount - amount in payment currency
  • order_id - a unique identifier for the order, you must generate it yourself
  • comment - comment on the order, available to the client (must be checked)
  • sci_id - store ID obtained when creating the store
  • sci_key - store secret key specified when creating the store
  • domain - domain specified when creating the store
  • test - test mode

You will receive a similar JSON in response:


{
  "error": false,
  "message": "Счет успешно выставлен",
  "data": {
    "url": "https://paykassa.pro/sci/redir.php?hash=HASH",
    "method": "GET",
    "params": {
      "hash": "HASH"
    }
  }
}

From it we are interested in the key error and data.url . If error equal false , then you can redirect the user to the address specified in data.url .


If you go to this address in non-test mode, you can see the payment page:


Страница оплаты


Payment Verification


After making the payment, PayKassa will contact you on the server at the address specified in the "Handler URL" field when creating the store. This request will contain only the order identifier, without its status. To get the status of the order (successfully processed or not) - you need to make a request indicating the identifier received:


curl https://paykassa.pro/sci/0.3/index.php -d 'func=sci_confirm_order&private_hash=PRIVATE_HASH&sci_id=SCI_ID&sci_key=SCI_KEY&domain=DOMAIN&test=true' -H 'Content-type: application/x-www-form-urlencoded'

Example for ruby:


    paykassa = Paykassa.new
    private_hash = params[:private_hash] # для RoR / sinatra
    result = paykassa.confirm_order(private_hash)

    # если произошла ошибка
    raise StandardError.new(result[:message]) if result[:error]

    order_id = res[:data][:order_id] # ID ордера
    amount = res[:data][:amount] # зачисленная сумма

Python example:


paykassa = PayKassa(0, '123', 'habratest.ngrok.io', False)
result = paykassa.confirm_order(request.POST['private_hash']) # пример для Django
if result['error']:
  print(result['message'])
else:
  print(result['data']['order_id'])
  print(result['data']['amount'])

Well, an example for node.js:


var PayKassa = require("./paykassa")
var paykassa = new Api({ sci_id: 0, sci_key: '123', domain: 'habratest.ngrok.io', test: true })
paykassa.sci_confirm_order({
    private_hash: req.body.private_hash // пример для express
  }, function (res) {
  if (res.error) {
    // если ошибка - кидаем Exception
    throw new Error(res.message)
  } else {
    // иначе получаем сумму и номер заказа
    console.log(res.data.order_id)
    console.log(res.data.amount)
  }
})

A similar JSON will come in response:


{"error":false,"message":"Платеж успешно подтвержден","data":{"transaction":"XXX","shop_id":"XXX","order_id":"1","amount":"0.01","currency":"BTC","system":"bitcoin","hash":"HASH"}}

Everything is standard here - if error equal false - it means that the payment for the order data.order_id for the amount data.amount was successful. data.amount contains the actual payment amount received. Depending on your logic of processing orders and making payments, sometimes it needs to be checked (for example, crediting funds where the user himself indicates the amount), sometimes not (for example, if the user can not directly influence the amount of the order placed or we credit the user’s balance excluding commission).


Also, it is worth noting that at about the same time the user is redirected to the successful / unsuccessful payment URL.


On this, the process of basic integration can be considered successfully completed. As you can see, the integration of the aggregator turned out to be much easier than the implementation of accepting payments yourself. If your volumes are less than $ 100- $ 1000 per month, then this method will most likely be more profitable for you.