Метод аутентификации запросов OAuth

OAuth HMAC-SHA1

Для формирования запроса с аутентификацией OAuth HMAC-SHA1 необходимо:

  1. Сформировать подпись,

  2. Сформировать заголовки,

  3. Сформировать запрос.

Формирование подписи OAuth HMAC-SHA1

Для подготовки подписи необходимо выполнить следующие действия:

  1. Собрать все параметры тела запроса и все параметры OAuth, включенные в тело запроса (oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, oauth_version):

    Значение oauth_consumer_key - логин торговца, предоставленный Платежным Шлюзом, например merchantlogin.
    Значение oauth_nonce - однократно используемая, сгенерированная случайным образом строка nonce, например y3qlvMPky7g.
    Значение oauth_signature_method - HMAC-SHA1.
    Значение oauth_timestamp - текущая временная метка в секундах, например 1669966913.
    Значение oauth_version - 1.0.
    Параметр oauth_signature в тело запроса не включен, он включен только в заголовок OAuth.
  2. Применить процентное кодирование (percent-encode) к значениям каждого собранного на 1 этапе параметра. Справочная информация содержится в RFC 3986. Незарезервированные символы могут быть закодированы, но не должны кодироваться. Зарезервированные символы должны быть закодированы, например: % кодируется в %25, / кодируется в %2F, = кодируется в %3D.

  3. Сортировать все собранные на этапе 1 параметры в лексикографическом порядке по названиям параметров.

  4. Соединить с помощью конкатенации отсортированные параметры и их значения с помощью символов & и =:
    object1=valueOfObject1&object2=valueOfObject2...
    Пример строки соединенных параметров:
    client-order-id=1234567890&oauth_consumer_key=merchantlogin&oauth_nonce=y3qlvMPky7g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1669966913&oauth_version=1.0&sending-card-ref-id=7654321
    
  5. Применить процентное кодирование к каждому элементу (POST, URL, строка соединенных параметров) и собрать базовую строку для подписи со структурой POST&URL&параметры (в соответствии с OAuth A.5.1.).
    Пример базовой строки для подписи:
    POST&https%3A%2F%2Fgate.payneteasy.com%2Fpaynet%2Fapi%2Fv2%2Fpan-eligibility%2Fsend%2FENDPOINTID&client-order-id%3D1234567890%26oauth_consumer_key%3Dmerchantlogin%26oauth_nonce%3Dy3qlvMPky7g%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1669966913%26oauth_version%3D1.0%26sending-card-ref-id%3D7654321
    
  6. Подпись строки с помощью HMAC-SHA1. Ключ подписи является конкатенацией consumer secret (контрольный ключ Торговца) + & + token secret (пустая строка).
    Пример ключа HMAC-SHA1:
    11111111-1111-1111-1111-111111111111&
    
  7. Закодировать полученную строку с помощью Base64 для получения подписи.
    Пример формирования подписи HMAC-SHA1 на Java:
    package com.Payneteasy;
    
    import org.apache.commons.codec.binary.Base64;
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    
    public class HMAC_SHA1 {
       private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
    
       public static String calculateHMAC(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException {
          SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
          Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
          mac.init(signingKey);
          return Base64.encodeBase64String(mac.doFinal(data.getBytes()));
       }
    }
    
    Пример подписи:
    d/IPlITUmPcniwjA7Vckjr6WQeE=
    

Формирование заголовков OAuth HMAC-SHA1

Запрос с аутентификацией OAuth HMAC-SHA1 должен иметь следующие заголовки:

  • content-type=application/x-www-form-urlencoded

  • Authorization: OAuth

Заголовок Authorization: OAuth должен содержать те же параметры и значения OAuth, которые добавлены в тело запроса и строку для подписи (oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, oauth_version). Также необходимо добавить подпись в качестве значения параметра oauth_signature в заголовок. Все значения параметров в заголовке Authorization: OAuth должны передаваться в кавычках (в соответствии с OAuth 5.4.1.) со структурой parameter=”value”, быть разделены обязательными запятыми (,) и необязательными пробелами. Параметр OAuth realm не является обязательным.
Пример заголовка Authorization (разделен построчно для удобства отображения):
Authorization: OAuth
oauth_consumer_key="merchantlogin",
oauth_nonce="y3qlvMPky7g",
oauth_signature="d/IPlITUmPcniwjA7Vckjr6WQeE=",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1669966913",
oauth_version="1.0"

Примечание

Т.к. заголовки запроса обычно не кодируются, значения oauth параметров (особенно oauth_nonce и oauth_signature) должны быть закодированы отдельно (percent-encode) перед добавлением в заголовок Authorization.

Формирование запроса OAuth HMAC-SHA1

  1. Необходимо использовать заголовки из предыдущей секции,

  2. Включить параметры в тело запроса,

  3. Применить процентное кодирование и отправить запрос.

Чтобы сформировать запрос:

Request method:      POST
Request URI: https://gate.payneteasy.com/paynet/api/v2/pan-eligibility/send/1111
Headers: Authorization=OAuth realm="",oauth_version="1.0",oauth_consumer_key="merchantlogin",oauth_timestamp="1669966913",oauth_nonce="y3qlvMPky7g",oauth_signature_method="HMAC-SHA1",oauth_signature="d%2FIPlITUmPcniwjA7Vckjr6WQeE%3D"
         Accept=*/*
         Content-Type=application/x-www-form-urlencoded; charset=ISO-8859-1
Body: client-order-id=1234567890&oauth_consumer_key=merchantlogin&oauth_nonce=y3qlvMPky7g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1669966913&oauth_version=1.0&sending-card-ref-id=7654321

Чтобы сформировать CURL запрос:

curl -H '
   Authorization: oauth_version="1.0",
   oauth_consumer_key="merchantlogin",
   oauth_timestamp="1669966913",
   oauth_nonce="y3qlvMPky7g",
   oauth_signature_method="HMAC-SHA1",
   oauth_signature="d%2FIPlITUmPcniwjA7Vckjr6WQeE%3D"
' --data '
   client-order-id=1234567890
   &oauth_consumer_key=merchantlogin
   &oauth_nonce=y3qlvMPky7g
   &oauth_signature_method=HMAC-SHA1
   &oauth_timestamp=1669966913
   &oauth_version=1.0
   &sending-card-ref-id=7654321
' 'https://gate.payneteasy.com/paynet/api/v2/pan-eligibility/send/ENDPOINTID'

OAuth RSA-SHA256

Для формирования запроса с аутентификацией OAuth RSA-SHA256 необходимо:

  1. Сгенерировать пару из публичного и приватного ключа,

  2. Сформировать подпись,

  3. Сформировать заголовки,

  4. Сформировать запрос.

Генерация ключей

Для отправки запросов с аутентификацией RSA-SHA256, необходимо сгенерировать пару из публичного и приватного ключа:

  • ПРИВАТНЫЙ ключ для подписания запросов. Этот ключ должен быть защищён от несанкционированного доступа.

  • ПУБЛИЧНЫЙ ключ для проверки, что запрос был подписан соответствующим приватным ключом. Этот ключ необходимо передать службе поддержки.

Для генерации ключей необходимо скачать последнюю версию утилиты openssl и запустить следующие команды:

openssl genpkey -algorithm RSA -out private_key_pkcs_8.pem -pkeyopt rsa_keygen_bits:4096
openssl rsa -pubout -in private_key_pkcs_8.pem -out public_key.pem

Необходимо использовать разные пары ключей для тестовой и производственной среды во избежание их компрометации.

Для использования инструментов формирования запросов и отладки в документации необходимо использовать ключ в контейнере PKCS#1 (незашифрованный приватный ключ RSA в текстовом формате PKCS#1 PEM). Такой ключ можно получить с помощью следующей команды:

openssl rsa -in private_key_pkcs_8.pem -out private_key_pkcs_1.pem

Для версий OpenSSL v3+ (проверено на v3.0.5) необходима другая команда:

openssl rsa -traditional -in private_key_pkcs_8.pem -out private_key_pkcs_1.pem

Получившийся приватный ключ PKCS#1 RSA будет начинаться со строки —–BEGIN RSA PRIVATE KEY—–. Для производственной среды подходит любой формат ключа, поддерживаемый программным комплексом Присоединяющейся стороны. Для инструментов формирования запросов и отладки в документации подходят только ключи с длиной до 4096. Пример приватного ключа PKCS#1 RSA:

-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEA16QK2iwgYUbMr2GqSbaS0PQZKF2DkstSj0dakW+hASTz5Ams
R5sDnurfeR4m+Htaxiv69MMdvoDLuCmZE8KQzsEOZovZ9UYSh9CKK4/FzQSZ8ZDP
8cpKLN7/gitWiM14iuC9Pi74TTLeg7PuGjeoc0jUs0WMf7sV6uzfZwvqYgUVRljY
gscwDRiTSGJQumQtanCs/LMIkxouThLztSSEmHhhEz2aWOomqR5hHO+HJ4I1AfET
V7VpKJ4c1+zMesDfpDxZ8VpQpno9iikFG64MigDFmBeskI6q15tBwbROYSfqNEmG
LwhYQ+SXnojueazkSJ45CeQRh6dn3GgD7kex2N3lK97qpqDcWOLqcsbe+ZyTGALn
WGzTZWjleO+yrdE6awD34kUYVnzD/9WvdYqpH2pXBDqOIXu6lm4gLe5pKTRiFEc+
TjgVb34tJGEERkrvqktSEmRQzMgZQnZk/5//7+csUIcSPmqdUn5oB6ngVueZkk7v
wtL6dcCxr5isWgXQEO+oYbt72Ns5RjLVWiXWv2ZNFd+iR4O6+etBxYNz+mg/2B5c
PO8NWyvvFlaBUu4I5GG1XntBGWncKQiZ49WCvLcYEbSfUEkWLj6zqJaDS/buT3jU
rWQ0WEI8G1HnTQp0cqmx9WpXDLx4n3ytRjHuHe3ND9AYE28yhfFY5baIU3UCAwEA
AQKCAgAC4QrQDOTFx7c15DzszQY6yfeIBW+bRyGsDgzUgkQJCuBCvCpTrmsm9QXU
zSVCDguRN8ca+3vrLjcKF2wWynM6f3NcxSM81hmrPIqLuFiwuw3/HqrYFJZW8QdC
SqfWHcAtQoDkUqY4CaTU51MXgIS8PU2xsw0EK5BIWa9F5e/ULTMyhD8nx9cJZbmZ
rs5bHrlIgYadvRoxNJlHq5MbaQhoLLtHEXx9EWtAuModI8mPKnrgssJKWn6z7yB9
dYjpXqfdvnyI72bCQkGOFaweyX0bXpVEyZQhPfZj+IuxNWIShADpf83N1POwvF2V
3Ugp0bgejBZA3o2pXP/S/oSG6ugh8dZHfa8vkw0x5N28393IzIMpzwE2EnsBidN1
ca7NwDpmpUyuULSpi3YoViUYY1i4Mwngv2XQdkbvoGusQwgWoNrppmDKxlL5qEBf
lIPCZAgZSR79KHYw2VOzkm84hu0jDXMthpt9A2gLkRhGnGgb4n5KzyCpY9iuFK2w
CO5FdjloXOjRLZb7G1JCeU6Qh0kjSE7seh9ltyo+VsWOLx4UwVOYGCMAF45yO0wF
/MJdYoUt1vC5G/DK8itTTjwb/xPlGDiC441TOReWVwF6n36+shb6szlI2EmqKBkp
2Sr5xQ5VNZkcG2W/BUF7+n8Rvisu17TyW0HmwDEBDfJQzgzgpQKCAQEA7RpBNVBH
Jx8gR7hQdbyi4/6U5KiYorkzuoK4KkjRWJfqJvp9uGZS2vDwOoIC8kCAXMwu3OCI
U0xkDvH7bb/qedn0IG7+72FUCKlxqkMk4lv03zE9yUPcYNT+w573uh+rXQ/mcsFF
+aBtupRZiDqqd4vuvjTpjw5Q4tyk/lxZfbe10S2NyxY4dZsbm8gl0SypLs/rLYjZ
8ZntRpZozIWoenrF3AnvtR114WBDpBVwSJ9KNd8xB5Fufc9TqsZ/EKPjDVrn2Sq2
Lt/xKSopwxPyIhKG1zmAeYhv8Q+GYUOQYCfBj2opDC3AOxANw2j9M8nYjCMDmPaP
5iDCUla35srp/wKCAQEA6NPj8auPGGFen2ZJoydpEPKgU3zAdv05VlKVIvbA9c+Y
oy7zNhnNw0PCkkYpB9jPGvpdn6KFh2ZTU/mgmIysKriLcLN4gKho7JUCU3kvg1mv
zJiz/5fR0xCCRNPLAANh6uJ+CXyssjbUoe9EmyVxKX3l2zKmy1zOKRc/FbAkql07
ItDReryb64IjsfT4GtU4nBK7zCzI+yya1BjL/McnGBcpKIwp9HCwaTQK7yxa7ThY
TsfTuxoyZM1/xZE0cKRJGVLtkao1VfOy0SDdCp+RwtBvVmt3Wt6vVcL6qG0LW5Fe
Uz0PN+CebMfhBCaqWXIXeuMUo+RdLnGn113Tl6i6iwKCAQEAz+NzRUGMAXtDHF84
/OJWmD1BY3OH0TU9a8ztmPWbyGf6gA6laKcfAqS6nTIdTzbK1ZKZjES6gv65xHjb
ERFyj0BQ0pc/o7fcrHOVG8ofbvFdtMxB9lQvyB84+WBKqMDXyZMFZZyctBC75Rnp
no6BpKvmupM+LZZJyX/YksV6GcaX/j5I0sY63rMO8/n7XnogJNFczOHu5e0mo/uB
C8ItRKadER8NM+oOz3tOE3JQrvwrXyzAmngjPuAn5daA1qA7lhwcqMbQUi08D/HO
CCNW7BT+cXsTcHv2WpBYLLPGxOhWyF42e10p7R9YUfud9miGG+kfYGDfLtGOUA+E
0zEbFQKCAQEA4lczDnqolpv5394RkiG6+zXTdLYfaM2NUwTfZOka9xxEl8cJuztk
lAIoggjg1HcKB4EDSTA2vUVVlppjbEm9CZ70N7DRYcnWjr/hTgLOlNO4mp6Mxdny
qkwvR/fZLf8bzrs2qcRhIrM5DN/NA0Jn+10f+nMIQUTMSpgFxPDDBDe0SIlWTApV
TaLrTpIGLBfCe7+ef8O98qgPMEeW7vswXzQM2BVCqBZw+SUVyCOHlXukJZoPlKHI
AcThBNC/eQ3M3miG+YfNZ+yMls9q82viyM/WnN3GXzmCnE37XYb8dp0gZK1EQR8F
BF1fu6hXDLNkbhuZsiZMC92DvFPDYnkuNwKCAQA6/2K8PLlOeK+0p/IGVsgJpgHn
Uh3BehVKHXeG/Buhn5bMXX3cB2hEHg2tz4pw3JxfZ1UflhyhKD43XnpxuMmt81Ka
Ja5MeXDg0kfnlXolVA4ezx2V2EohMExUykkOIfQBDTaNtjsg5PB4HLKFId3kJ6u/
JCXuy0EA07vl/kNl+cDEBLJsVtvtxHLdpdJhO1POi3IIgOpddO+a/O/GDsdlAWog
hyEb6r7+bWurjw0YjHX+R5ZQ+0XtnzXU20d2NiP/oH2IvQzXRUQ1U17Kzzn5PAhs
YC7r9lRV4VjbhEi3Zk2FBPrrzs2ieXo5aHXCnzFywQ99nlrz0Ic8vV16WR1x
-----END RSA PRIVATE KEY-----

Формирование подписи OAuth RSA-SHA256

Для подготовки подписи необходимо выполнить следующие действия:

  1. Собрать все параметры тела запроса и все параметры OAuth, включенные в тело запроса (oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, oauth_version):

    Значение oauth_consumer_key - логин торговца, предоставленный Платежным Шлюзом, например merchantlogin.
    Значение oauth_nonce - однократно используемая, сгенерированная случайным образом строка nonce, например y3qlvMPky7g.
    Значение oauth_signature_method - RSA-SHA256.
    Значение oauth_timestamp - текущая временная метка в секундах, например 1669966913.
    Значение oauth_version - 1.0.
    Параметр oauth_signature в тело запроса не включен, он включен только в заголовок OAuth.
  2. Применить процентное кодирование (percent-encode) к значениям каждого собранного на 1 этапе параметра. Справочная информация содержится в RFC 3986. Незарезервированные символы могут быть закодированы, но не должны кодироваться. Зарезервированные символы должны быть закодированы, например: % кодируется в %25, / кодируется в %2F, = кодируется в %3D.

  3. Сортировать все собранные на этапе 1 параметры в лексикографическом порядке по названиям параметров.

  4. Соединить с помощью конкатенации отсортированные параметры и их значения с помощью символов & и =:object1=valueOfObject1&object2=valueOfObject2...
  5. Применить процентное кодирование к каждому элементу (POST, URL, строка соединенных параметров) и собрать базовую строку для подписи со структурой POST&URL&параметры (в соответствии с OAuth A.5.1.).
    Пример базовой строки для подписи:
    POST&https%3A%2F%2Fgate.payneteasy.com%2Fpaynet%2Fapi%2Fv4%2Ftransfer%2FENDPOINTID&amount%3D10.42%26card_printed_name%3DJohn%2520Doe%26card_recurring_payment_id%3D42322%26client_orderid%3D1%26credit_card_number%3D4210708776705721%26currency%3DUSD%26cvv2%3D123%26deposit2card%3Dfalse%26destination-card-no%3D4232618181101636%26destination_card_recurring_payment_id%3D61622%26expire_month%3D12%26expire_year%3D2099%26ipaddress%3D1.1.1.1.1%26oauth_consumer_key%3Dpaydroid%26oauth_nonce%3DhoFlZri9c17X1Tvb7yD2fsMEQUIWBQ3m%26oauth_signature_method%3DRSA-SHA256%26oauth_timestamp%3D1669720957%26oauth_version%3D1.0%26order_desc%3DYour%2520order%2520description%26redirect_url%3Dhttp%253A%252F%252Fwww.example.com
    
  6. Подписать полученную строку для подписи с помощью RSA-SHA256 используя приватный ключ, получение которого описано в предыдущей секции.
  7. Закодировать полученную строку с помощью Base64 для получения подписи.
    Пример подписи, закодированной в Base64:
    K0hLc7GYh65UDTNvJvJbqoD95T7ekVEwIx+AxLBe2rNndPVzCAZMTi58J5pJlXZA1qOrgzUj/uL764NofP6qrqBXHX9Fpg+PdoMBey7zY9nMOmtdpHhkwyqA0n8e8oh68x+8RtC1+gmaIsIJDVurpCm2CdaViC2ny90GWPrrSin9CFwDmIKBtOJ7dxNnuFQJkvLxwK9JE9gRfQssG4vOrXrn2f5DvENFvFW3fL7meiN3mKuBFyEHIv2cibWopoUTQrxAgCfTHvRuU5nRIct9oWgCYLYzROOPyAIjtyDFKcTnUql9+tD2+p2rMDDU7HqJUGy764rb4ShuvuiuEvzIaNwg3JAxho9fqcKJz5LXt4efX1i8oFt33ztYSgZRojsoW4HCzuhZcQQmRexpmtCGYKqH3Q2BsG2jIkQAxL9BOUOzXNoeXoVQIf3+47cJ0KujEHuDXROblq3o9Uos5K+Mu9Carjs8jHMiBHo4aS4IAgXVY3mEuohCuRhL9/Y9buuyvdKSNsao7qHwD/6bb9Sj2MFFhYP6gHf+p1NipH05224aX9hMZty8Ovb3+ps/wNWYC8NVfft5bh8ETowaHQ5TUOPFdvU+5IkHOnnfbvz1/+jDeErd0Pdq4xH6c5/gZVQY9j6DFpazsw8d3karwXoBduv7I3mh7L3CSjTweABaRMw=
    

Формирование заголовков OAuth RSA-SHA256

Запрос с аутентификацией OAuth RSA-SHA256 должен иметь следующие заголовки:

  • content-type=application/x-www-form-urlencoded

  • Authorization: OAuth

Заголовок Authorization: OAuth должен содержать те же параметры и значения OAuth, которые добавлены в тело запроса и строку для подписи (oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, oauth_version). Также необходимо добавить подпись в качестве значения параметра oauth_signature в заголовок. Все значения параметров в заголовке Authorization: OAuth должны передаваться в кавычках (в соответствии с OAuth 5.4.1.) со структурой parameter=”value”, быть разделены обязательными запятыми (,) и необязательными пробелами. Параметр OAuth realm не является обязательным.
Пример заголовка Authorization (разделен построчно для удобства отображения):
Authorization: OAuth
oauth_consumer_key="paydroid",
oauth_nonce="hoFlZri9c17X1Tvb7yD2fsMEQUIWBQ3m",
oauth_signature="K0hLc7GYh65UDTNvJvJbqoD95T7ekVEwIx%2BAxLBe2rNndPVzCAZMTi58J5pJlXZA1qOrgzUj%2FuL764NofP6qrqBXHX9Fpg%2BPdoMBey7zY9nMOmtdpHhkwyqA0n8e8oh68x%2B8RtC1%2BgmaIsIJDVurpCm2CdaViC2ny90GWPrrSin9CFwDmIKBtOJ7dxNnuFQJkvLxwK9JE9gRfQssG4vOrXrn2f5DvENFvFW3fL7meiN3mKuBFyEHIv2cibWopoUTQrxAgCfTHvRuU5nRIct9oWgCYLYzROOPyAIjtyDFKcTnUql9%2BtD2%2Bp2rMDDU7HqJUGy764rb4ShuvuiuEvzIaNwg3JAxho9fqcKJz5LXt4efX1i8oFt33ztYSgZRojsoW4HCzuhZcQQmRexpmtCGYKqH3Q2BsG2jIkQAxL9BOUOzXNoeXoVQIf3%2B47cJ0KujEHuDXROblq3o9Uos5K%2BMu9Carjs8jHMiBHo4aS4IAgXVY3mEuohCuRhL9%2FY9buuyvdKSNsao7qHwD%2F6bb9Sj2MFFhYP6gHf%2Bp1NipH05224aX9hMZty8Ovb3%2Bps%2FwNWYC8NVfft5bh8ETowaHQ5TUOPFdvU%2B5IkHOnnfbvz1%2F%2BjDeErd0Pdq4xH6c5%2FgZVQY9j6DFpazsw8d3karwXoBduv7I3mh7L3CSjTweABaRMw%3D",
oauth_signature_method="RSA-SHA256",
oauth_timestamp="1669720957",
oauth_version="1.0"

Примечание

Т.к. заголовки запроса обычно не кодируются, значения oauth параметров (особенно oauth_nonce и oauth_signature) должны быть закодированы отдельно (percent-encode) перед добавлением в заголовок Authorization.

Формирование запроса OAuth RSA-SHA256

Чтобы сформировать запрос:

  1. Необходимо использовать заголовки из предыдущей секции,

  2. Включить параметры в тело запроса,

  3. Применить процентное кодирование и отправить запрос.

Чтобы сформировать запрос:

Request method: POST
Request URI: https://gate.payneteasy.com/paynet/api/v4/transfer/4473
Headers: Authorization= OAuth oauth_nonce="C12mCYiI4RDeVTvyZDHS3f0vUbBNrK9G", oauth_signature="S4ahUgep8EV3RTIPdcpO%2FzEwB8V1XwFcGi5zhrYKySPVVA2PAY7AxezPcOCMhMwZQfcf8VOH8O9v5zZ%2BmYV2Qsq4kjPe1zEJIjjdjhI%2B2MX9VW8dWn9DyoTD2lkOYUwGsCteXU6mwGtNergN5KwGTJgqYPfzWFLllSAhGwuOd%2FgHYVRnA6jd4FvywRaRiSsnzgsasGJjWCGzX%2F1B8L78H%2FqD1W7dNJHjC2JbonUiHVT4aOs74qpqYE9zcb5rK8PH1l2ems3ArXhqFaSF85%2BYzJ%2BkSeDswqeIZ1y3NS8XPrmaLeymLNqKpOfAl9Ng47AmIjSYEw3s5fQ7Xi9t4j7Y6fQA4RnIzIdmH4oeMiyzn7dpA87wqnXm5AIm6Den2TJaDg90UoCMXuGHkqfL8GGUSjaWleTOHlk%2FO4dBGDRw4LP1aEaJktQRmT5xwyoaQfz%2Bh2MR7zDpRbVZDpUto1iYlQl5UlgojOHaLLjW1gqggbvrtVXUBT73KcIboWW00VUcbVUX5Yb%2FSu7hUZO2fNAh4LYrsVcXRJxCZuwhXCiqvbR3EziEFDGVEZnKUDLCodzHbVA9hbpxHGa7TvSLFEANCPDFtapMF9eo%2F1yOt5Tkxag4g5yNes93lrVabsTqckiFIfKR2R3CcwJ6MQtMfo8ticJcKxfo2v%2BTkC0bEaRkBGk%3D", oauth_consumer_key="ny_qa_merchant", oauth_signature_method="RSA-SHA256", oauth_timestamp="1669892964", oauth_version="1.0"
         Accept=*/*
         Content-Type=application/x-www-form-urlencoded; charset=ISO-8859-1
Body: cvv2=123&ipaddress=1.1.1.1&amount=15.42&credit_card_number=4210708776705721&client_orderid=1&deposit2card=false&destination-card-no=4232618181101636&expire_month=12&order_desc=Your%20order%20description&card_printed_name=Vasia%20Pupkin&currency=RUB&expire_year=2099&redirect_url=http%3A%2F%2Fwww.example.com

Чтобы сформировать CURL запрос:

curl -H '
   Authorization: OAuth oauth_consumer_key="paydroid",
   oauth_nonce="JOARJWjXJgfRUVqig8HIs0ouFWKfK4N5",
   oauth_signature="k5Z0XCVdDvb5h873XG6TDMO854PsuueSnSby4h0%2F3j4TKraY6ebjoFfDmndI09%2FQp6uEbCgKregNY5N0ccIVIay49l6v7jMdIFEfqU7E5eu%2BIJcoqG7kMFcdCu29hweYx7p4ZSk%2FUtdGlN3wyUUybCAx73XYoO0tkZteAleyzQlzdpzQ99vPS8FN2WMbNdzU3H2PLf0XOZy4DbAPleZGfu3GWxXe9erGsvzBJozs3WFxiPFeULfzWWsNc1h1P7cnzNbZqXkI%2BV3qiG3jc7tDqGRZP%2BLZFw3nihNlW%2F2Nlp%2FId6QG8kPNOEx2GxAuQa8kufv%2BbpohU8UftZG1SnNfoa4nDVgNbWoSbTXbgXxwHE0ZlccwT6q%2BHbTmgggvIdGN9JuuLUL8fDSCxqu3R7YMvcwArzrkd1XEFUFvYWxHc3QpfbBu0GyLkL7pnmz%2BFPTH48COZ7yXbK6nQDFSIy8lmaEJsnnjMYyMMVgjNkDNzqwNxOQuwprTZ7KzHqWrTj0zHTOHl44q1pJhIqMwxWDPMsziExYbAzxuOkdQFZi%2BK%2Bu3M7tvG5Foy7Vwj%2BDSPMPAhj7j2AmUG4HzaJcpiBWMq0CkGWRjvneOk3NiwkEwOxq5tJOFjsroditUqJQSX1PTqf%2FtuiqaE2Gt5EYl19ZzyXbxtMjGZwL%2BzF1cT6ftTi4%3D",
   oauth_signature_method="RSA-SHA256",
   oauth_timestamp="1669892687",
   oauth_version="1.0"
' --data '
   amount=10.42
   &card_printed_name=John%20Doe
   &card_recurring_payment_id=42322
   &client_orderid=1
   &credit_card_number=4210708776705721
   &currency=USD
   &cvv2=123
   &deposit2card=false
   &destination-card-no=4232618181101636
   &destination_card_recurring_payment_id=61622
   &expire_month=12
   &expire_year=2099
   &ipaddress=1.1.1.1.1
   &order_desc=Your%20order%20description
   &redirect_url=http%3A%2F%2Fwww.example.com
' 'https://gate.payneteasy.com/paynet/api/v4/transfer/ENDPOINTID'

Формирование запроса OAuth RSA-SHA256

<?php
/**
 * OAuth RSA-SHA256 Integration Example for PaynetEasy API
 * This example demonstrates a simplified single-file implementation
 */

// Include the OAuth library (tmhOAuth)
class tmhOAuth {
  const VERSION = '0.8.5';
  var $response = array();

  /**
   * Creates a new tmhOAuth object
   *
   * @param string $config, the configuration to use for this request
   * @return void
   */
  public function __construct($config=array()) {
    $this->buffer = null;
    $this->reconfigure($config);
    $this->reset_request_settings();
    $this->set_user_agent();
  }

  public function reconfigure($config=array()) {
    // default configuration options
    $this->config = array_merge(
      array(
        // leave 'user_agent' blank for default, otherwise set this to
        // something that clearly identifies your app
        'user_agent'                 => '',
        'host'                       => 'api.twitter.com',
        'method'                     => 'GET',

        'consumer_key'               => '',
        'consumer_secret'            => '',
        'token'                      => '',
        'secret'                     => '',

        // RSA private key (for RSA-SHA1 and RSA-SHA256 methods)
        // Please note that this is expected to be a string representing
        // the PEM-formatted key itself and NOT the file name
        'private_key_pem'            => '',

        // OAuth2 bearer token. This should already be URL encoded
        'bearer'                     => '',

        // oauth signing variables that are not dynamic
        'oauth_version'              => '1.0',
        'oauth_signature_method'     => 'HMAC-SHA1',

        // you probably don't want to change any of these curl values
        'curl_http_version'          => CURL_HTTP_VERSION_1_1,
        'curl_connecttimeout'        => 30,
        'curl_timeout'               => 10,

        // for security this should always be set to 2.
        'curl_ssl_verifyhost'        => 2,
        // for security this should always be set to true.
        'curl_ssl_verifypeer'        => true,
        // for security this should always be set to true.
        'use_ssl'                    => true,

        // you can get the latest cacert.pem from here http://curl.haxx.se/ca/cacert.pem
        // if you're getting HTTP 0 responses, check cacert.pem exists and is readable
        // without it curl won't be able to create an SSL connection
        'curl_cainfo'                => __DIR__ . DIRECTORY_SEPARATOR . 'cacert.pem',
        'curl_capath'                => __DIR__,

        // in some cases (very very odd ones) the SSL version must be set manually.
        // unless you know why your are changing this, you should leave it as false
        // to allow PHP to determine the value for this setting itself.
        'curl_sslversion'            => false,

        'curl_followlocation'        => false, // whether to follow redirects or not

        // support for proxy servers
        'curl_proxy'                 => false, // really you don't want to use this if you are using streaming
        'curl_proxyuserpwd'          => false, // format username:password for proxy, if required
        'curl_encoding'              => '',    // leave blank for all supported formats, else use gzip, deflate, identity etc

        // streaming API configuration
        'is_streaming'               => false,
        'streaming_eol'              => "\r\n",
        'streaming_metrics_interval' => 10,

        // header or querystring. You should always use header!
        // this is just to help me debug other developers implementations
        'as_header'                  => true,
        'force_nonce'                => false, // used for checking signatures. leave as false for auto
        'force_timestamp'            => false, // used for checking signatures. leave as false for auto
      ),
      $config
    );
  }

  private function reset_request_settings($options=array()) {
    $this->request_settings = array(
      'params'    => array(),
      'headers'   => array(),
      'with_user' => true,
      'multipart' => false,
    );

    if (!empty($options))
      $this->request_settings = array_merge($this->request_settings, $options);
  }

  /**
   * Sets the useragent for PHP to use
   * If '$this->config['user_agent']' already has a value it is used instead of one
   * being generated.
   *
   * @return void value is stored to the config array class variable
   */
  private function set_user_agent() {
    if (!empty($this->config['user_agent']))
      return;

    $ssl = ($this->config['curl_ssl_verifyhost'] && $this->config['curl_ssl_verifypeer'] && $this->config['use_ssl']) ? '+' : '-';
    $ua = 'tmhOAuth ' . self::VERSION . $ssl . 'SSL - //github.com/themattharris/tmhOAuth';
    $this->config['user_agent'] = $ua;
  }

  /**
   * Generates a random OAuth nonce.
   * If 'force_nonce' is false a nonce will be generated, otherwise the value of '$this->config['force_nonce']' will be used.
   *
   * @param string $length how many characters the nonce should be before MD5 hashing. default 12
   * @param string $include_time whether to include time at the beginning of the nonce. default true
   * @return $nonce as a string
   */
  private function nonce($length=12, $include_time=true) {
    if ($this->config['force_nonce'] === false) {
      $prefix = $include_time ? microtime() : '';
      return md5(substr($prefix . uniqid(), 0, $length));
    } else {
      return $this->config['force_nonce'];
    }
  }

  /**
   * Generates a timestamp.
   * If 'force_timestamp' is false a timestamp will be generated, otherwise the value of '$this->config['force_timestamp']' will be used.
   *
   * @return $time as a string
   */
  private function timestamp() {
    if ($this->config['force_timestamp'] === false) {
      $time = time();
    } else {
      $time = $this->config['force_timestamp'];
    }
    return (string) $time;
  }

  /**
   * Encodes the string or array passed in a way compatible with OAuth.
   * If an array is passed each array value will will be encoded.
   *
   * @param mixed $data the scalar or array to encode
   * @return $data encoded in a way compatible with OAuth
   */
  private function safe_encode($data) {
    if (is_array($data)) {
      return array_map(array($this, 'safe_encode'), $data);
    } else if (is_scalar($data)) {
      return str_ireplace(
        array('+', '%7E'),
        array(' ', '~'),
        rawurlencode($data)
      );
    } else {
      return '';
    }
  }

  /**
   * Decodes the string or array from it's URL encoded form
   * If an array is passed each array value will will be decoded.
   *
   * @param mixed $data the scalar or array to decode
   * @return string $data decoded from the URL encoded form
   */
  private function safe_decode($data) {
    if (is_array($data)) {
      return array_map(array($this, 'safe_decode'), $data);
    } else if (is_scalar($data)) {
      return rawurldecode($data);
    } else {
      return '';
    }
  }

  /**
   * Prepares OAuth1 signing parameters.
   *
   * @return void all required OAuth parameters, safely encoded, are stored to the class variable '$this->request_settings['oauth1_params']'
   */
  private function prepare_oauth1_params() {
    $defaults = array(
      'oauth_nonce'            => $this->nonce(),
      'oauth_timestamp'        => $this->timestamp(),
      'oauth_version'          => $this->config['oauth_version'],
      'oauth_consumer_key'     => $this->config['consumer_key'],
      'oauth_signature_method' => $this->config['oauth_signature_method'],
    );

    // include the user token if it exists
    if ( $oauth_token = $this->token() )
      $defaults['oauth_token'] = $oauth_token;

    $this->request_settings['oauth1_params'] = array();

    // safely encode
    foreach ($defaults as $k => $v) {
      $this->request_settings['oauth1_params'][$this->safe_encode($k)] = $this->safe_encode($v);
    }
  }

  private function token() {
    if ( $this->request_settings['with_user'] ) {
      if (isset($this->config['token']) && !empty($this->config['token'])) return $this->config['token'];
      elseif (isset($this->config['user_token'])) return $this->config['user_token'];
    }
    return '';
  }

  private function secret() {
    if ( $this->request_settings['with_user'] ) {
      if (isset($this->config['secret']) && !empty($this->config['secret'])) return $this->config['secret'];
      elseif (isset($this->config['user_secret'])) return $this->config['user_secret'];
    }
    return '';
  }

  /**
   * Extracts and decodes OAuth parameters from the passed string
   *
   * @param string $body the response body from an OAuth flow method
   * @return array the response body safely decoded to an array of key => values
   */
  public function extract_params($body) {
    $kvs = explode('&', $body);
    $decoded = array();
    foreach ($kvs as $kv) {
      $kv = explode('=', $kv, 2);
      $kv[0] = $this->safe_decode($kv[0]);
      $kv[1] = $this->safe_decode($kv[1]);
      $decoded[$kv[0]] = $kv[1];
    }
    return $decoded;
  }

  /**
   * Prepares the HTTP method for use in the base string by converting it to
   * uppercase.
   *
   * @return void value is stored to the class variable '$this->request_settings['method']'
   */
  private function prepare_method() {
    $this->request_settings['method'] = strtoupper($this->request_settings['method']);
  }

  /**
   * Prepares the URL for use in the base string by ripping it apart and
   * reconstructing it.
   *
   * Ref: 3.4.1.2
   *
   * @return void value is stored to the class array variable '$this->request_settings['url']'
   */
  private function prepare_url() {
    $parts = parse_url($this->request_settings['url']);

    $port   = isset($parts['port']) ? $parts['port'] : false;
    $scheme = $parts['scheme'];
    $host   = $parts['host'];
    $path   = isset($parts['path']) ? $parts['path'] : false;

    $port or $port = ($scheme == 'https') ? '443' : '80';

    if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80')) {
      $host = "$host:$port";
    }

    // the scheme and host MUST be lowercase
    $this->request_settings['url'] = strtolower("$scheme://$host");
    // but not the path
    $this->request_settings['url'] .= $path;
  }

  /**
   * If the request uses multipart, and the parameter isn't a file path, prepend a space
   * otherwise return the original value. we chose a space here as twitter whitespace trims from
   * the beginning of the tweet. we don't use \0 here because it's the character for string
   * termination.
   *
   * @param the parameter value
   * @return string the original or modified string, depending on the request and the input parameter
   */
  private function multipart_escape($value) {
    if (!$this->request_settings['multipart'] || strpos($value, '@') !== 0)
      return $value;

    // see if the parameter is a file.
    // we split on the semi-colon as it's the delimiter used on media uploads
    // for fields with semi-colons this will return the original string
    list($file) = explode(';', substr($value, 1), 2);
    if (file_exists($file))
      return $value;

    return " $value";
  }


  /**
   * Prepares all parameters for the base string and request.
   * Multipart parameters are ignored as they are not defined in the specification,
   * all other types of parameter are encoded for compatibility with OAuth.
   *
   * @param array $params the parameters for the request
   * @return void prepared values are stored in the class array variable '$this->request_settings'
   */
  private function prepare_params() {
    $doing_oauth1 = false;
    $this->request_settings['prepared_params'] = array();
    $prepared = &$this->request_settings['prepared_params'];
    $prepared_pairs = array();
    $prepared_pairs_with_oauth = array();

    if (isset($this->request_settings['oauth1_params'])) {
      $oauth1  = &$this->request_settings['oauth1_params'];
      $doing_oauth1 = true;
      $params = array_merge($oauth1, $this->request_settings['params']);

      // Remove oauth_signature if present
      // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
      unset($params['oauth_signature']);

      // empty the oauth1 array. we reset these values later in this method
      $oauth1 = array();
    } else {
      $params = $this->request_settings['params'];
    }

    // Parameters are sorted by name, using lexicographical byte value ordering.
    // Ref: Spec: 9.1.1 (1)
    uksort($params, 'strcmp');

    // set this now so we're not doing it on every parameter
    $supports_curl_file = class_exists('CurlFile', false);

    // encode params unless we're doing multipart
    foreach ($params as $k => $v) {
      $k = $this->request_settings['multipart'] ? $k : $this->safe_encode($k);

      if (is_array($v))
        $v = implode(',', $v);

      // we don't need to do the multipart escaping if we support curlfile
      if ($supports_curl_file && ($v instanceof CurlFile)) {
        // leave $v alone
      } elseif ($this->request_settings['multipart']) {
        $v = $this->multipart_escape($v);
      } else {
        $v = $this->safe_encode($v);
      }

      // split parameters for the basestring and authorization header, and recreate the oauth1 array
      if ($doing_oauth1) {
        // if we're doing multipart, only store the oauth_* params, ignore the users request params
        if ((strpos($k, 'oauth') === 0) || !$this->request_settings['multipart'])
          $prepared_pairs_with_oauth[] = "{$k}={$v}";

        if (strpos($k, 'oauth') === 0) {
          $oauth1[$k] = $v;
          continue;
        }
      }
      $prepared[$k] = $v;

      if (!$this->request_settings['multipart'])
        $prepared_pairs[] = "{$k}={$v}";
    }

    if ($doing_oauth1) {
      $this->request_settings['basestring_params'] = implode('&', $prepared_pairs_with_oauth);
    }

    // setup params for GET/POST/PUT method handling
    if (!empty($prepared)) {
      $content = implode('&', $prepared_pairs);

      switch ($this->request_settings['method']) {
        case 'PUT':
          // fall through to POST as PUT should be treated the same
        case 'POST':
          $this->request_settings['postfields'] = $this->request_settings['multipart'] ? $prepared : $content;
          break;
        default:
          $this->request_settings['querystring'] = $content;
          break;
      }
    }
  }

  /**
   * Prepares the OAuth signing key
   *
   * @return void prepared signing key is stored in the class variable 'signing_key'
   */
  private function prepare_signing_key() {
    $left = $this->safe_encode($this->config['consumer_secret']);
    $right = $this->safe_encode($this->secret());
    $this->request_settings['signing_key'] = $left . '&' . $right;
  }

  /**
   * Prepare the base string.
   * Ref: Spec: 9.1.3 ("Concatenate Request Elements")
   *
   * @return void prepared base string is stored in the class variable 'base_string'
   */
  private function prepare_base_string() {
    $url = $this->request_settings['url'];

    # if the host header is set we need to rewrite the basestring to use
    # that, instead of the request host. otherwise the signature won't match
    # on the server side
    if (!empty($this->request_settings['headers']['Host'])) {
      $url = str_ireplace(
        $this->config['host'],
        $this->request_settings['headers']['Host'],
        $url
      );
    }

    $base = array(
      $this->request_settings['method'],
      $url,
      $this->request_settings['basestring_params']
    );
    $this->request_settings['basestring'] = implode('&', $this->safe_encode($base));
  }

  /**
   * Signs the OAuth 1 request
   *
   * @return void oauth_signature is added to the parameters in the class array variable '$this->request_settings'
   */
  private function prepare_oauth_signature() {
    switch ($this->config['oauth_signature_method']) {
      case 'HMAC-SHA1':
        $signature = $this->sign_with_hmac('sha1');
        break;
      case 'HMAC-SHA256':
        $signature = $this->sign_with_hmac('sha256');
        break;
      case 'RSA-SHA1':
        $signature = $this->sign_with_rsa(OPENSSL_ALGO_SHA1);
        break;
      case 'RSA-SHA256':
        $signature = $this->sign_with_rsa(OPENSSL_ALGO_SHA256);
        break;
      default:
        throw new Exception("Unsupported oauth_signature_method: '" . $this->config['oauth_signature_method'] . "'");
    }
    $this->request_settings['oauth1_params']['oauth_signature'] = $this->safe_encode(base64_encode($signature));
  }

  /**
   * Signs the OAuth 1 request using HMAC-based signature algorithm
   *
   * @param string $algorithm algorithm name (like sha1 or sha256)
   * @return binary signature
   */
  private function sign_with_hmac($algorithm) {
    return hash_hmac(
      $algorithm, $this->request_settings['basestring'], $this->request_settings['signing_key'], true
    );
  }

  /**
   * Signs the OAuth 1 request using RSA-based signature algorithm
   *
   * @param mixed $algorithm ID or name of hash algorithm that will be
   * used to compute base string hash before encrypting it with RSA;
   * values understood by openssl_sign()'s $signature_alg parameter
   * are accepted here (like 'sha1' or OPENSSL_ALGO_SHA256)
   * @return binary signature
   */
  private function sign_with_rsa($algorithm) {
    if (!function_exists('openssl_sign')) {
      throw new Exception("openssl_sign function does not exist. Please make sure Openssl extension is installed");
    }
    if ($this->config['private_key_pem'] == '') {
      throw new Exception("No private key PEM is configured, cannot sign");
    }
    $ok = openssl_sign($this->request_settings['basestring'], $signature, $this->config['private_key_pem'], $algorithm);
    if (!$ok) {
      throw new Exception("Cannot sign: " . openssl_error_string());
    }
    return $signature;
  }

  /**
   * Prepares the Authorization header
   *
   * @return void prepared authorization header is stored in the class variable headers['Authorization']
   */
  private function prepare_auth_header() {
    if (!$this->config['as_header'])
      return;

    // oauth1
    if (isset($this->request_settings['oauth1_params'])) {
      // sort again as oauth_signature was added post param preparation
      uksort($this->request_settings['oauth1_params'], 'strcmp');
      $encoded_quoted_pairs = array();
      foreach ($this->request_settings['oauth1_params'] as $k => $v) {
        $encoded_quoted_pairs[] = "{$k}=\"{$v}\"";
      }
      $header = 'OAuth ' . implode(', ', $encoded_quoted_pairs);
    } elseif (!empty($this->config['bearer'])) {
      $header = 'Bearer ' . $this->config['bearer'];
    }

    if (isset($header))
      $this->request_settings['headers']['Authorization'] = $header;
  }

  /**
   * Create the bearer token for OAuth2 requests from the consumer_key and consumer_secret.
   *
   * @return string the bearer token
   */
  public function bearer_token_credentials() {
    $credentials = implode(':', array(
      $this->safe_encode($this->config['consumer_key']),
      $this->safe_encode($this->config['consumer_secret'])
    ));
    return base64_encode($credentials);
  }

  /**
   * Make an HTTP request using this library. This method doesn't return anything.
   * Instead the response should be inspected directly.
   *
   * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
   * @param string $url the request URL without query string parameters
   * @param array $params the request parameters as an array of key=value pairs. Default empty array
   * @param string $useauth whether to use authentication when making the request. Default true
   * @param string $multipart whether this request contains multipart data. Default false
   * @param array $headers any custom headers to send with the request. Default empty array
   * @return int the http response code for the request. 0 is returned if a connection could not be made
   */
  public function request($method, $url, $params=array(), $useauth=true, $multipart=false, $headers=array()) {
    $options = array(
      'method'    => $method,
      'url'       => $url,
      'params'    => $params,
      'with_user' => true,
      'multipart' => $multipart,
      'headers'   => $headers
    );
    $options = array_merge($this->default_options(), $options);

    if ($useauth) {
      return $this->user_request($options);
    } else {
      return $this->unauthenticated_request($options);
    }
  }

  public function apponly_request($options=array()) {
    $options = array_merge($this->default_options(), $options, array(
      'with_user' => false,
    ));
    $this->reset_request_settings($options);
    if ($options['without_bearer']) {
      return $this->oauth1_request();
    } else {
      $this->prepare_method();
      $this->prepare_url();
      $this->prepare_params();
      $this->prepare_auth_header();
      return $this->curlit();
    }
  }

  public function user_request($options=array()) {
    $options = array_merge($this->default_options(), $options, array(
      'with_user' => true,
    ));
    $this->reset_request_settings($options);
    return $this->oauth1_request();
  }

  public function unauthenticated_request($options=array()) {
    $options = array_merge($this->default_options(), $options, array(
      'with_user' => false,
    ));
    $this->reset_request_settings($options);
    $this->prepare_method();
    $this->prepare_url();
    $this->prepare_params();
    return $this->curlit();
  }

  /**
   * Signs the request and adds the OAuth signature. This runs all the request
   * parameter preparation methods.
   *
   * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
   * @param string $url the request URL without query string parameters
   * @param array $params the request parameters as an array of key=value pairs
   * @param boolean $with_user whether to include the user credentials when making the request.
   * @return void
   */
  private function oauth1_request() {
    $this->prepare_oauth1_params();
    $this->prepare_method();
    $this->prepare_url();
    $this->prepare_params();
    $this->prepare_base_string();
    $this->prepare_signing_key();
    $this->prepare_oauth_signature();
    $this->prepare_auth_header();
    return $this->curlit();
  }

  private function default_options() {
    return array(
      'method'         => 'GET',
      'params'         => array(),
      'with_user'      => true,
      'multipart'      => false,
      'headers'        => array(),
      'without_bearer' => false,
    );
  }

  /**
   * Make a long poll HTTP request using this library. This method is
   * different to the other request methods as it isn't supposed to disconnect
   *
   * Using this method expects a callback which will receive the streaming
   * responses.
   *
   * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
   * @param string $url the request URL without query string parameters
   * @param array $params the request parameters as an array of key=value pairs
   * @param string $callback the callback function to stream the buffer to.
   * @return void
   */
  public function streaming_request($method, $url, $params=array(), $callback='') {
    if ( ! empty($callback) ) {
      if ( ! is_callable($callback) ) {
        return false;
      }
      $this->config['streaming_callback'] = $callback;
    }
    $this->metrics['start']          = time();
    $this->metrics['interval_start'] = $this->metrics['start'];
    $this->metrics['messages']       = 0;
    $this->metrics['last_messages']  = 0;
    $this->metrics['bytes']          = 0;
    $this->metrics['last_bytes']     = 0;
    $this->config['is_streaming']    = true;
    $this->request($method, $url, $params);
  }

  /**
   * Handles the updating of the current Streaming API metrics.
   *
   * @return array the metrics for the streaming api connection
   */
  private function update_metrics() {
    $now = time();
    if (($this->metrics['interval_start'] + $this->config['streaming_metrics_interval']) > $now)
      return null;

    $this->metrics['mps'] = round( ($this->metrics['messages'] - $this->metrics['last_messages']) / $this->config['streaming_metrics_interval'], 2);
    $this->metrics['bps'] = round( ($this->metrics['bytes'] - $this->metrics['last_bytes']) / $this->config['streaming_metrics_interval'], 2);

    $this->metrics['last_bytes'] = $this->metrics['bytes'];
    $this->metrics['last_messages'] = $this->metrics['messages'];
    $this->metrics['interval_start'] = $now;
    return $this->metrics;
  }

  /**
   * Utility function to create the request URL in the requested format.
   * If a fully-qualified URI is provided, it will be returned.
   * Any multi-slashes (except for the protocol) will be replaced with a single slash.
   *
   *
   * @param string $request the API method without extension
   * @param string $extension the format of the response. Default json. Set to an empty string to exclude the format
   * @return string the concatenation of the host, API version, API method and format, or $request if it begins with http
   */
  public function url($request, $extension='json') {
    // remove multi-slashes
    $request = preg_replace('$([^:])//+$', '$1/', $request);

    if (stripos($request, 'http') === 0 || stripos($request, '//') === 0) {
      return $request;
    }

    $extension = strlen($extension) > 0 ? ".$extension" : '';
    $proto  = $this->config['use_ssl'] ? 'https:/' : 'http:/';

    // trim trailing slash
    $request = ltrim($request, '/');

    $pos = strlen($request) - strlen($extension);
    if (substr($request, $pos) === $extension)
      $request = substr_replace($request, '', $pos);

    return implode('/', array(
      $proto,
      $this->config['host'],
      $request . $extension
    ));
  }

  /**
   * Public access to the private safe decode/encode methods
   *
   * @param string $text the text to transform
   * @param string $mode the transformation mode. either encode or decode
   * @return string $text transformed by the given $mode
   */
  public function transformText($text, $mode='encode') {
    return $this->{"safe_$mode"}($text);
  }

  /**
   * Utility function to parse the returned curl headers and store them in the
   * class array variable.
   *
   * @param object $ch curl handle
   * @param string $header the response headers
   * @return string the length of the header
   */
  private function curlHeader($ch, $header) {
    $this->response['raw'] .= $header;

    list($key, $value) = array_pad(explode(':', $header, 2), 2, null);

    $key = trim($key);
    $value = trim($value);

    if ( ! isset($this->response['headers'][$key])) {
      $this->response['headers'][$key] = $value;
    } else {
      if (!is_array($this->response['headers'][$key])) {
        $this->response['headers'][$key] = array($this->response['headers'][$key]);
      }
      $this->response['headers'][$key][] = $value;
    }

    return strlen($header);
  }

  /**
    * Utility function to parse the returned curl buffer and store them until
    * an EOL is found. The buffer for curl is an undefined size so we need
    * to collect the content until an EOL is found.
    *
    * This function calls the previously defined streaming callback method.
    *
    * @param object $ch curl handle
    * @param string $data the current curl buffer
    * @return int the length of the data string processed in this function
    */
  private function curlWrite($ch, $data) {
    $l = strlen($data);
    if (strpos($data, $this->config['streaming_eol']) === false) {
      $this->buffer .= $data;
      return $l;
    }

    $buffered = explode($this->config['streaming_eol'], $data);
    $content = $this->buffer . $buffered[0];

    $this->metrics['messages']++;
    $this->metrics['bytes'] += strlen($content);

    if ( ! is_callable($this->config['streaming_callback']))
      return 0;

    $metrics = $this->update_metrics();
    $stop = call_user_func(
      $this->config['streaming_callback'],
      $content,
      strlen($content),
      $metrics
    );
    $this->buffer = $buffered[1];
    if ($stop)
      return 0;

    return $l;
  }

  /**
   * Makes a curl request. Takes no parameters as all should have been prepared
   * by the request method
   *
   * the response data is stored in the class variable 'response'
   *
   * @return int the http response code for the request. 0 is returned if a connection could not be made
   */
  private function curlit() {
    $this->response = array(
      'raw' => ''
    );

    // configure curl
    $c = curl_init();

    if ($this->request_settings['method'] == 'GET' && isset($this->request_settings['querystring'])) {
      $this->request_settings['url'] = $this->request_settings['url'] . '?' . $this->request_settings['querystring'];
    } elseif ($this->request_settings['method'] == 'POST' || $this->request_settings['method'] == 'PUT') {
      $postfields = array();
      if (isset($this->request_settings['postfields']))
        $postfields = $this->request_settings['postfields'];

      curl_setopt($c, CURLOPT_POSTFIELDS, $postfields);
    }

    curl_setopt($c, CURLOPT_CUSTOMREQUEST, $this->request_settings['method']);

    curl_setopt_array($c, array(
      CURLOPT_HTTP_VERSION   => $this->config['curl_http_version'],
      CURLOPT_USERAGENT      => $this->config['user_agent'],
      CURLOPT_CONNECTTIMEOUT => $this->config['curl_connecttimeout'],
      CURLOPT_TIMEOUT        => $this->config['curl_timeout'],
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_SSL_VERIFYPEER => $this->config['curl_ssl_verifypeer'],
      CURLOPT_SSL_VERIFYHOST => $this->config['curl_ssl_verifyhost'],

      CURLOPT_FOLLOWLOCATION => $this->config['curl_followlocation'],
      CURLOPT_PROXY          => $this->config['curl_proxy'],
      CURLOPT_ENCODING       => $this->config['curl_encoding'],
      CURLOPT_URL            => $this->request_settings['url'],
      // process the headers
      CURLOPT_HEADERFUNCTION => array($this, 'curlHeader'),
      CURLOPT_HEADER         => false,
      CURLINFO_HEADER_OUT    => true,
    ));

    if ($this->config['curl_cainfo'] !== false)
      curl_setopt($c, CURLOPT_CAINFO, $this->config['curl_cainfo']);

    if ($this->config['curl_capath'] !== false)
      curl_setopt($c, CURLOPT_CAPATH, $this->config['curl_capath']);

    if ($this->config['curl_proxyuserpwd'] !== false)
      curl_setopt($c, CURLOPT_PROXYUSERPWD, $this->config['curl_proxyuserpwd']);

    if ($this->config['curl_sslversion'] !== false)
      curl_setopt($c, CURLOPT_SSLVERSION, $this->config['curl_sslversion']);

    if ($this->config['is_streaming']) {
      // process the body
      $this->response['content-length'] = 0;
      curl_setopt($c, CURLOPT_TIMEOUT, 0);
      curl_setopt($c, CURLOPT_WRITEFUNCTION, array($this, 'curlWrite'));
    }

    if ( ! empty($this->request_settings['headers'])) {
      foreach ($this->request_settings['headers'] as $k => $v) {
        $headers[] = trim($k . ': ' . $v);
      }
      curl_setopt($c, CURLOPT_HTTPHEADER, $headers);
    }

    if (isset($this->config['block']) && (true === $this->config['block']))
      return 0;

    // do it!
    $response = curl_exec($c);
    $code = curl_getinfo($c, CURLINFO_HTTP_CODE);
    $info = curl_getinfo($c);
    $error = curl_error($c);
    $errno = curl_errno($c);
    curl_close($c);

    // store the response
    $this->response['code'] = $code;
    $this->response['response'] = $response;
    $this->response['info'] = $info;
    $this->response['error'] = $error;
    $this->response['errno'] = $errno;

    if (!isset($this->response['raw'])) {
      $this->response['raw'] = '';
    }
    $this->response['raw'] .= $response;

    return $code;
  }
}

/*
 * Configuration - Replace these values with your actual credentials
 */
$config = [
    'server_host' => 'sandbox.payneteasy.com',
    'endpoint_id' => 1, // Your endpoint ID
    'merchant_login' => 'your-merchant-login',
    'private_key_file' => 'test-private-key.pem', // Your private key file
    'test_mode' => true // Set to false for production
];

/*
 * API Request Function
 */
function makeApiRequest($endpoint, $params, $config) {
    // Load private key
    $private_key_pem = file_get_contents($config['private_key_file']);
    if (!$private_key_pem) {
        throw new Exception("Could not load private key file");
    }

    // Initialize OAuth
    $oauth = new tmhOAuth();
    $oauth->reconfigure([
        'consumer_key' => $config['merchant_login'],
        'oauth_signature_method' => 'RSA-SHA256',
        'private_key_pem' => $private_key_pem,
        'curl_ssl_verifypeer' => $config['test_mode'] ? false : true
    ]);

    // Make request
    $url = "https://{$config['server_host']}/paynet/api/v4/{$endpoint}/{$config['endpoint_id']}";
    $status = $oauth->request('POST', $url, $params);

    // Handle response
    if ($status == 0) {
        throw new Exception('HTTP request failed');
    } elseif ($status != 200) {
        throw new Exception("HTTP error: {$status}");
    }

    return [
        'status' => $status,
        'response' => $oauth->response['response'],
        'headers' => $oauth->response['headers']
    ];
}

/*
 * Example Usage - Transfer by Reference
 */
if (isset($_POST['action']) && $_POST['action'] == 'transfer') {
    try {
        $params = [
            'client-order-id' => 'transfer-' . uniqid(),
            'amount' => $_POST['amount'] ?? '100',
            'currency' => $_POST['currency'] ?? 'RUB',
            'order_desc' => $_POST['description'] ?? 'Consumer credit',
            'destination-card-no' => $_POST['card_number'] ?? '4444333322221111'
        ];

        $result = makeApiRequest('transfer-by-ref', $params, $config);
        $response = json_decode($result['response'], true);

        echo "<div class='alert alert-success'>Success! Transaction ID: " .
             ($response['transaction-id'] ?? 'N/A') . "</div>";

    } catch (Exception $e) {
        echo "<div class='alert alert-danger'>Error: " . $e->getMessage() . "</div>";
    }
}

/*
 * Example Usage - Bank Payout
 */
if (isset($_POST['action']) && $_POST['action'] == 'payout') {
    try {
        $params = [
            'client-order-id' => 'payout-' . uniqid(),
            'amount' => $_POST['amount'] ?? '100',
            'currency' => $_POST['currency'] ?? 'USD',
            'order_desc' => $_POST['description'] ?? 'Bank payout',
            'account_number' => $_POST['account_number'] ?? '123412341234',
            'bank_name' => $_POST['bank_name'] ?? 'Test Bank'
        ];

        $result = makeApiRequest('payout', $params, $config);
        $response = json_decode($result['response'], true);

        echo "<div class='alert alert-success'>Success! Payout ID: " .
             ($response['payout-id'] ?? 'N/A') . "</div>";

    } catch (Exception $e) {
        echo "<div class='alert alert-danger'>Error: " . $e->getMessage() . "</div>";
    }
}
?>