Authentication
All requests must contain the following headers:
| Header | Description |
|---|---|
| X-Api-Key | Your API key (SHA256 from public key). |
| X-Api-Signature | The query's serialized body signed by your private key according to the RSA-SHA256 method. |
Authenticate and send signed requests
The following code samples illustrate how to authenticate and send signed requests.
- Node.js
- Python
- PHP
- Go
- Java
- Ruby
- Rust
- .NET
info
Requires Node.js >= 18.
const crypto = require('crypto');
const privateKeyString = '<<Your private key>>';
const privateKey = crypto.createPrivateKey({
key: Buffer.from(privateKeyString, 'hex'),
format: 'der',
type: 'pkcs8',
});
const publicKey = crypto.createPublicKey(privateKey).export({
type: 'pkcs1',
format: 'der',
});
const message = {
jsonrpc: '2.0',
id: 'test',
method: 'getCurrencies',
};
const body = JSON.stringify(message);
const signature = crypto.sign('sha256', Buffer.from(body), privateKey);
(async () => {
const res = await fetch('https://api.changelly.com/v2', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': crypto.createHash('sha256').update(publicKey).digest('base64'),
'X-Api-Signature': signature.toString('base64'),
},
body,
});
console.log(await res.text());
})();
info
Requires Python >= 3.9.
import base64
import binascii
import json
import logging
from typing import List
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from requests import post, Response
logging.basicConfig(level=logging.INFO)
class ApiException(Exception):
def __init__(self, code: int, message: str):
self.code = code
self.message = message
class ApiService:
def __init__(self, url: str, private_key: str, x_api_key: str):
self.url = url
self.private_key = private_key
self.x_api_key = x_api_key
def _request(self, method: str, params: dict or list = None) -> Response or List[dict]:
params = params if params else {}
message = {
'jsonrpc': '2.0',
'id': 'test',
'method': method,
'params': params
}
response = post(self.url, headers=self._get_headers(body=message), json=message)
if response.ok:
response_body = response.json()
logging.info(f'{method} response: {response_body} (request: {params})')
if response_body.get('error'):
error = response_body['error']
raise ApiException(error['code'], error['message'])
return response_body['result']
raise ApiException(response.status_code, response.text)
def _sign_request(self, body: dict) -> bytes:
decoded_private_key = binascii.unhexlify(self.private_key)
private_key = RSA.import_key(decoded_private_key)
message = json.dumps(body).encode('utf-8')
h = SHA256.new(message)
signature = pkcs1_15.new(private_key).sign(h)
return base64.b64encode(signature)
def _get_headers(self, body: dict) -> dict:
signature = self._sign_request(body)
return {
'content-type': 'application/json',
'X-Api-Key': self.x_api_key,
'X-Api-Signature': signature,
}
def get_pairs_params(self, currency_from: str, currency_to: str):
return self._request('getPairsParams', params=[{'from': currency_from, 'to': currency_to}])
api = ApiService(
url='https://api.changelly.com/v2/',
private_key='<<Your private key>>',
x_api_key='<<SHA256 hash of your public key in Base64 format>>',
)
api.get_pairs_params('eth', 'btc')
info
Requires PHP 8.
<?php
require __DIR__ . "/vendor/autoload.php";
use phpseclib3\Crypt\RSA;
class ApiException extends Exception { }
class ApiService {
private $url;
private $privateKey;
private $apiKey;
public function __construct($url, $privateKeyData, $apiKey) {
$this->url = $url;
$this->apiKey = $apiKey;
$privateKeyData = hex2bin($privateKeyData);
$this->privateKey = RSA::load($privateKeyData);
}
private function request($method, $params = null) {
$params = $params ?? [];
$message = [
'jsonrpc' => '2.0',
'id' => 'test',
'method' => $method,
'params' => $params
];
$response = $this->post($this->url, [
'headers' => $this->getHeaders($message),
'json' => $message
]);
if ($response['httpCode'] == 200) {
if (isset($response['body']['error'])) {
$error = $response['body']['error'];
throw new ApiException($error['message'], $error['code']);
}
return $response['body']['result'];
}
throw new ApiException($response['body'] ?? '', $response['httpCode']);
}
private function signRequest($body) {
$message = json_encode($body);
openssl_sign($message, $signature, $this->privateKey, OPENSSL_ALGO_SHA256);
return base64_encode($signature);
}
private function getHeaders($body) {
$signature = $this->signRequest($body);
return [
'content-type: application/json',
'X-Api-Key: ' . $this->apiKey,
'X-Api-Signature: ' . $signature
];
}
private function post($url, $data) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data['json']),
CURLOPT_HTTPHEADER => $data['headers']
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'body' => json_decode($response, true),
'httpCode' => $httpCode
];
}
public function getPairsParams($currencyFrom, $currencyTo) {
return $this->request('getPairsParams', [['from' => $currencyFrom, 'to' => $currencyTo]]);
}
public function getTransactions() {
return $this->request('getTransactions');
}
}
$privateKey = '<<Your private key>>';
$apiKey = '<<SHA256 hash of your public key in Base64 format>>';
$api = new ApiService(
'https://api.changelly.com/v2/',
$privateKey,
$apiKey
);
try {
$limits = $api->getPairsParams('eth', 'btc');
var_dump($limits);
$transactions = $api->getTransactions();
var_dump($transactions);
} catch (ApiException $error) {
echo "Error with code " . $error->getCode() . PHP_EOL;
if ($error->getMessage()) {
echo "Message: " . $error->getMessage() . PHP_EOL;
}
} catch (Exception $error) {
echo 'Error: ' . $error->getMessage() . PHP_EOL;
}
info
Requires Go 1.21.1.
package main
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
// Your generated private key as a string
privateKeyString := "<<YOUR_PRIVATE_KEY>>"
// Decode the hex-encoded private key
decodedPrivateKey, err := hex.DecodeString(privateKeyString)
if err != nil {
panic(err)
}
// Parse the PKCS8 private key
privateKey, err := x509.ParsePKCS8PrivateKey(decodedPrivateKey)
if err != nil {
panic(err)
}
rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey)
if !ok {
panic("Failed to cast to RSA Private Key")
}
// Marshal the public key to DER format
publicDER := x509.MarshalPKCS1PublicKey(&rsaPrivateKey.PublicKey)
// Define the message payload
message := map[string]interface{}{
"jsonrpc": "2.0",
"id": "test",
"method": "getCurencies",
}
messageBytes, err := json.Marshal(message)
if err != nil {
panic(err)
}
// Sign the message with the RSA private key
hashed := sha256.Sum256(messageBytes)
signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, crypto.SHA256, hashed[:])
if err != nil {
panic(err)
}
// Prepare the HTTP POST request
client := &http.Client{}
req, err := http.NewRequest("POST", "https://api.changelly.com/v2", bytes.NewReader(messageBytes))
if err != nil {
panic(err)
}
// Set request headers
hash := sha256.Sum256(publicDER)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", base64.StdEncoding.EncodeToString(hash[:]))
req.Header.Set("X-Api-Signature", base64.StdEncoding.EncodeToString(signature))
// Send the request
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// Check if the status code is 401
if resp.StatusCode == http.StatusUnauthorized {
fmt.Println("Received status code 401 Unauthorized")
}
// Read and display the response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(respBody))
}
info
Recommend java version is >= 11.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class ApiIntegration {
private static final String PRIVATE_KEY_HEX = "your_private_key";
public static void main(String[] args) throws Exception {
byte[] pkcs8Bytes = hexToBytes(PRIVATE_KEY_HEX);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(
new PKCS8EncodedKeySpec(wrapPkcs1InPkcs8IfNeeded(pkcs8Bytes))
);
java.security.interfaces.RSAPrivateCrtKey crtKey =
(java.security.interfaces.RSAPrivateCrtKey) privateKey;
PublicKey publicKey = keyFactory.generatePublic(
new java.security.spec.RSAPublicKeySpec(
crtKey.getModulus(), crtKey.getPublicExponent()
)
);
byte[] spkiDer = publicKey.getEncoded();
byte[] pkcs1Der = new byte[spkiDer.length - 24];
System.arraycopy(spkiDer, 24, pkcs1Der, 0, pkcs1Der.length);
byte[] publicKeyHash = MessageDigest.getInstance("SHA-256").digest(pkcs1Der);
String xApiKey = Base64.getEncoder().encodeToString(publicKeyHash);
String message = "{\"jsonrpc\":\"2.0\",\"id\":\"test\",\"method\":\"getCurrencies\"}";
byte[] body = message.getBytes(StandardCharsets.UTF_8);
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey);
signer.update(body);
String xApiSignature = Base64.getEncoder().encodeToString(signer.sign());
HttpClient client = HttpClient.newBuilder()
.sslContext(trustAllSslContext())
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.changelly.com/v2"))
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.header("Content-Type", "application/json")
.header("X-Api-Key", xApiKey)
.header("X-Api-Signature", xApiSignature)
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
private static byte[] hexToBytes(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
private static byte[] wrapPkcs1InPkcs8IfNeeded(byte[] der) {
byte[] pkcs8Header = new byte[] {
0x30, (byte)0x82, 0x00, 0x00,
0x02, 0x01, 0x00,
0x30, 0x0d,
0x06, 0x09,
0x2a, (byte)0x86, 0x48, (byte)0x86, (byte)0xf7, 0x0d, 0x01, 0x01, 0x01,
0x05, 0x00,
0x04, (byte)0x82, 0x00, 0x00
};
if (der.length > 10 && der[4] == 0x02 && der[9] == 0x06) {
return der;
}
int pkcs1Len = der.length;
int totalLen = pkcs8Header.length + pkcs1Len;
int outerContentLen = totalLen - 4;
pkcs8Header[2] = (byte) ((outerContentLen >> 8) & 0xff);
pkcs8Header[3] = (byte) (outerContentLen & 0xff);
pkcs8Header[pkcs8Header.length - 2] = (byte) ((pkcs1Len >> 8) & 0xff);
pkcs8Header[pkcs8Header.length - 1] = (byte) (pkcs1Len & 0xff);
byte[] pkcs8 = new byte[totalLen];
System.arraycopy(pkcs8Header, 0, pkcs8, 0, pkcs8Header.length);
System.arraycopy(der, 0, pkcs8, pkcs8Header.length, pkcs1Len);
return pkcs8;
}
private static SSLContext trustAllSslContext() throws Exception {
TrustManager[] trustAll = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
public void checkClientTrusted(X509Certificate[] c, String a) {}
public void checkServerTrusted(X509Certificate[] c, String a) {}
}
};
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustAll, new SecureRandom());
return ctx;
}
}
info
Recommend ruby version is >= 2.6.
require 'net/http'
require 'uri'
require 'json'
require 'openssl'
require 'digest'
require 'base64'
PRIVATE_KEY_HEX = 'your_private_key'
private_key_der = [PRIVATE_KEY_HEX].pack('H*')
private_key = OpenSSL::PKey.read(private_key_der)
spki_der = private_key.public_key.to_der
public_key_der = spki_der[24..]
x_api_key = Base64.strict_encode64(Digest::SHA256.digest(public_key_der))
message = {
jsonrpc: '2.0',
id: 'test',
method: 'getCurrencies'
}
body = JSON.generate(message)
signature = private_key.sign(OpenSSL::Digest::SHA256.new, body)
x_api_signature = Base64.strict_encode64(signature)
uri = URI.parse('https://api.changelly.com/v2')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path)
request['Content-Type'] = 'application/json'
request['X-Api-Key'] = x_api_key
request['X-Api-Signature'] = x_api_signature
request.body = body
response = http.request(request)
puts response.body
info
Tested on rust version 1.88.0
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use pkcs8::DecodePrivateKey;
use rsa::{pkcs1::EncodeRsaPublicKey, RsaPrivateKey};
use rsa::signature::{RandomizedSigner, SignatureEncoding};
use rsa::pkcs1v15::SigningKey;
use sha2::{Digest, Sha256};
const PRIVATE_KEY_HEX: &str = "YOUR_PRIVATE_KEY";
fn main() -> Result<(), Box<dyn std::error::Error>> {
let private_key_bytes = hex::decode(PRIVATE_KEY_HEX)?;
let private_key = RsaPrivateKey::from_pkcs8_der(&private_key_bytes)?;
let public_key = private_key.to_public_key();
let public_key_pkcs1_der = public_key.to_pkcs1_der()?;
let public_key_hash = Sha256::digest(public_key_pkcs1_der.as_bytes());
let x_api_key = BASE64.encode(public_key_hash);
let message = serde_json::json!({
"jsonrpc": "2.0",
"id": "test",
"method": "getCurrencies",
});
let body = serde_json::to_vec(&message)?;
let signing_key = SigningKey::<Sha256>::new(private_key);
let mut rng = rand::thread_rng();
let signature = signing_key.sign_with_rng(&mut rng, &body);
let x_api_signature = BASE64.encode(signature.to_bytes());
let client = reqwest::blocking::Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
let response = client
.post("https://api.changelly.com/v2")
.header("Content-Type", "application/json")
.header("X-Api-Key", &x_api_key)
.header("X-Api-Signature", &x_api_signature)
.body(body)
.send()?;
println!("{}", response.text()?);
Ok(())
}
info
Tested on .NET version 10.0
using System.Net.Http;
using System.Security.Cryptography;
using System.Text.Json;
const string PRIVATE_KEY_HEX = "YOUR_PRIVATE_KEY";
byte[] pkcs8Bytes = Convert.FromHexString(PRIVATE_KEY_HEX);
using RSA rsa = RSA.Create();
rsa.ImportPkcs8PrivateKey(pkcs8Bytes, out _);
byte[] pkcs1Der = rsa.ExportRSAPublicKey();
byte[] publicKeyHash = SHA256.HashData(pkcs1Der);
string xApiKey = Convert.ToBase64String(publicKeyHash);
var message = new { jsonrpc = "2.0", id = "test", method = "getCurrencies" };
byte[] body = JsonSerializer.SerializeToUtf8Bytes(message);
byte[] signatureBytes = rsa.SignData(body, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
string xApiSignature = Convert.ToBase64String(signatureBytes);
using HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
};
using HttpClient client = new(handler);
using HttpRequestMessage request = new(HttpMethod.Post, "https://api.changelly.com/v2")
{
Content = new ByteArrayContent(body),
};
request.Content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
request.Headers.Add("X-Api-Key", xApiKey);
request.Headers.Add("X-Api-Signature", xApiSignature);
HttpResponseMessage response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());