Reference Client
A small helper you copy once into your project and then reuse for every authenticated request. It handles signing, the auth headers, and the timestamp for you, so you don't have to think about authentication again.
Every endpoint page in this documentation calls these helpers — sign_request(...) (Python / Go), client.send(...) (Java), or signRequest(...) (Node.js). If your code matches what's on the page, it will work as shown.
How it works
Given a method, path, optional query parameters, and optional body, the helper:
- Builds the URL-decoded path with query string.
- Computes
signed_message = METHOD + path + epoch. - Signs the message with your Ed25519 secret key.
- Returns the headers (with the signature, API key, and epoch) plus the final URL path you should hit.
You then make the HTTP request as normal.
For the exact bytes that get signed, see Authentication.
The helper
- Python
- Java
- Go
- Node.js
import time
import urllib.parse
import requests
from cryptography.hazmat.primitives.asymmetric import ed25519
API_KEY = "<your api key>" # hex
SECRET_KEY = "<your secret key>" # hex
BASE_URL = "https://coinswitch.co"
def sign_request(method, path, params=None):
"""
Build the headers and final URL path for an authenticated CoinSwitch
request. Pass the returned dict as `headers=` and the returned `path`
as the URL (after BASE_URL).
method — "GET" / "POST" / "DELETE"
path — endpoint path, e.g. "/trade/api/v2/order"
params — query parameters dict (for GET requests with filters)
"""
method = method.upper()
if params:
sep = "&" if "?" in path else "?"
path = path + sep + urllib.parse.urlencode(params)
decoded_path = urllib.parse.unquote_plus(path)
epoch = str(int(time.time() * 1000))
message = method + decoded_path + epoch
secret = ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(SECRET_KEY))
signature = secret.sign(message.encode("utf-8")).hex()
headers = {
"Content-Type": "application/json",
"X-AUTH-APIKEY": API_KEY,
"X-AUTH-SIGNATURE": signature,
"X-AUTH-EPOCH": epoch,
}
return headers, decoded_path
Required:
- Python 3.9+
cryptography(for Ed25519):pip install cryptographyrequests(or any HTTP client):pip install requests
package com.example;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.util.encoders.Hex;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class CoinswitchClient {
private static final String BASE_URL = "https://coinswitch.co";
private static final String API_KEY = "<your api key>"; // hex
private static final String SECRET_KEY = "<your secret key>"; // hex
private final HttpClient http = HttpClient.newHttpClient();
private final ObjectMapper json = new ObjectMapper();
/** Authenticated request. body may be null for GET. */
public HttpResponse<String> send(String method,
String path,
Map<String, String> params,
Map<String, Object> body) throws Exception {
method = method.toUpperCase();
if (params != null && !params.isEmpty()) {
StringBuilder qs = new StringBuilder();
for (Map.Entry<String, String> e : params.entrySet()) {
if (qs.length() > 0) qs.append("&");
qs.append(URLEncoder.encode(e.getKey(), "UTF-8"));
qs.append("=");
qs.append(URLEncoder.encode(e.getValue(), "UTF-8"));
}
path += (path.contains("?") ? "&" : "?") + qs;
}
String decodedPath = URLDecoder.decode(path, "UTF-8");
String epoch = String.valueOf(System.currentTimeMillis());
String message = method + decodedPath + epoch;
String signature = signEd25519(SECRET_KEY, message);
HttpRequest.Builder req = HttpRequest.newBuilder()
.uri(new URI(BASE_URL + decodedPath))
.header("Content-Type", "application/json")
.header("X-AUTH-APIKEY", API_KEY)
.header("X-AUTH-SIGNATURE", signature)
.header("X-AUTH-EPOCH", epoch);
String bodyJson = (body == null) ? "" : json.writeValueAsString(body);
switch (method) {
case "GET": req.GET(); break;
case "POST": req.POST(BodyPublishers.ofString(bodyJson)); break;
case "DELETE": req.method("DELETE", BodyPublishers.ofString(bodyJson)); break;
case "PUT": req.PUT(BodyPublishers.ofString(bodyJson)); break;
default: throw new IllegalArgumentException("Unsupported method: " + method);
}
return http.send(req.build(), HttpResponse.BodyHandlers.ofString());
}
private static String signEd25519(String secretKeyHex, String message) {
byte[] secretBytes = Hex.decode(secretKeyHex);
Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(secretBytes, 0);
Ed25519Signer signer = new Ed25519Signer();
signer.init(true, privateKey);
byte[] msg = message.getBytes(StandardCharsets.UTF_8);
signer.update(msg, 0, msg.length);
return Hex.toHexString(signer.generateSignature());
}
}
Required:
- Java 11+
- BouncyCastle:
org.bouncycastle:bcprov-jdk18on:1.78(for Ed25519) - Jackson:
com.fasterxml.jackson.core:jackson-databind:2.17.0(for JSON)
package coinswitch
import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
const (
BaseURL = "https://coinswitch.co"
APIKey = "<your api key>" // hex
SecretKey = "<your secret key>" // hex
)
// SignRequest builds headers and the final URL path for an authenticated
// CoinSwitch request. Use the returned path with BaseURL on your HTTP call.
//
// method — "GET" / "POST" / "DELETE"
// path — endpoint path, e.g. "/trade/api/v2/order"
// params — query parameters (for GET filters); nil if none
func SignRequest(method, path string, params map[string]string) (
headers map[string]string, decodedPath string, err error,
) {
method = strings.ToUpper(method)
if len(params) > 0 {
q := url.Values{}
for k, v := range params {
q.Set(k, v)
}
sep := "?"
if strings.Contains(path, "?") {
sep = "&"
}
path = path + sep + q.Encode()
}
decodedPath, err = url.QueryUnescape(path)
if err != nil {
return nil, "", fmt.Errorf("decode path: %w", err)
}
epoch := strconv.FormatInt(time.Now().UnixMilli(), 10)
message := method + decodedPath + epoch
secretBytes, err := hex.DecodeString(SecretKey)
if err != nil {
return nil, "", fmt.Errorf("decode secret: %w", err)
}
// ed25519.NewKeyFromSeed expects a 32-byte seed; the Ed25519 private
// key on file is the 32-byte seed.
priv := ed25519.NewKeyFromSeed(secretBytes)
signature := hex.EncodeToString(ed25519.Sign(priv, []byte(message)))
headers = map[string]string{
"Content-Type": "application/json",
"X-AUTH-APIKEY": APIKey,
"X-AUTH-SIGNATURE": signature,
"X-AUTH-EPOCH": epoch,
}
return headers, decodedPath, nil
}
Required:
- Go 1.21+
- Standard library only —
crypto/ed25519,net/url,encoding/hexship with Go.
// reference-client.js (CommonJS — for ESM, change require → import)
const crypto = require('crypto');
const API_KEY = '<your api key>'; // hex
const SECRET_KEY = '<your secret key>'; // hex
const BASE_URL = 'https://coinswitch.co';
/**
* Build the headers and final URL path for an authenticated CoinSwitch
* request. Use the returned path with BASE_URL on your HTTP call.
*
* method — "GET" / "POST" / "DELETE"
* path — endpoint path, e.g. "/trade/api/v2/order"
* params — query params object; null if none
*/
function signRequest(method, path, params = null) {
method = method.toUpperCase();
if (params && Object.keys(params).length) {
const qs = new URLSearchParams(params).toString();
const sep = path.includes('?') ? '&' : '?';
path = path + sep + qs;
}
const decodedPath = decodeURIComponent(path.replace(/\+/g, ' '));
const epoch = String(Date.now());
const message = method + decodedPath + epoch;
// Ed25519 private key in DER format from the 32-byte hex seed.
const seed = Buffer.from(SECRET_KEY, 'hex');
const der = Buffer.concat([
Buffer.from('302e020100300506032b657004220420', 'hex'),
seed,
]);
const privateKey = crypto.createPrivateKey({key: der, format: 'der', type: 'pkcs8'});
const signature = crypto.sign(null, Buffer.from(message, 'utf8'), privateKey).toString('hex');
const headers = {
'Content-Type': 'application/json',
'X-AUTH-APIKEY': API_KEY,
'X-AUTH-SIGNATURE': signature,
'X-AUTH-EPOCH': epoch,
};
return {headers, path: decodedPath};
}
module.exports = {signRequest, BASE_URL};
Required:
- Node.js 18+ (uses native
fetchandcrypto.signwith Ed25519). - No external dependencies. (For HTTP,
fetchis built-in on Node 18+; if you're on Node 16, installnode-fetchoraxios.)
Usage
- Python
- Java
- Go
- Node.js
# Place a spot order
body = {
"side": "sell",
"symbol": "BTC/USDT",
"type": "limit",
"price": 26000,
"quantity": 0.0009,
"exchange": "c2c1",
}
headers, path = sign_request("POST", "/trade/api/v2/order")
response = requests.post(BASE_URL + path, headers=headers, json=body)
print(response.json())
import java.math.BigDecimal;
HashMap<String, Object> body = new HashMap<>();
body.put("side", "sell");
body.put("symbol", "BTC/USDT");
body.put("type", "limit");
body.put("price", new BigDecimal("26000"));
body.put("quantity", new BigDecimal("0.0009"));
body.put("exchange", "c2c1");
CoinswitchClient client = new CoinswitchClient();
HttpResponse<String> resp = client.send(
"POST", "/trade/api/v2/order", null, body);
System.out.println(resp.body());
import (
"bytes"
"encoding/json"
"io"
"net/http"
)
body := map[string]any{
"side": "sell",
"symbol": "BTC/USDT",
"type": "limit",
"price": 26000,
"quantity": 0.0009,
"exchange": "c2c1",
}
bodyJSON, _ := json.Marshal(body)
headers, path, err := SignRequest("POST", "/trade/api/v2/order", nil)
if err != nil { panic(err) }
req, _ := http.NewRequest("POST", BaseURL+path, bytes.NewReader(bodyJSON))
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
const {signRequest, BASE_URL} = require('./reference-client');
const body = {
side: 'sell',
symbol: 'BTC/USDT',
type: 'limit',
price: 26000,
quantity: 0.0009,
exchange: 'c2c1',
};
const {headers, path} = signRequest('POST', '/trade/api/v2/order');
const r = await fetch(BASE_URL + path, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
console.log(await r.json());
HFT and Futures notes
- HFT: same helper, different base URL — change
BASE_URLtohttps://dma.coinswitch.co. HFT requiresX-AUTH-EPOCH(the helper always sends it, so you're fine). - Futures: same base URL, just hit
/trade/api/v2/futures/...paths.
WebSocket signing
Spot and Futures WebSockets (wss://ws.coinswitch.co/...) are public-only at the socket layer — the connection itself doesn't require a signature. The auth happens in the Socket.IO handshake's auth.apiKey, which is your CoinSwitch API key. Private streams (Spot order updates, Spot balance updates) read your API key from there. See the per-page guides under Spot › WebSockets.
For HFT private NATS streams, you fetch credentials from a REST endpoint and pass them to the WebSocket auth message. The helper below wraps that fetch.
- Python
- Java
- Go
- Node.js
# Requires the sign_request helper above.
def get_socket_credentials(expires_in_seconds=300):
"""
Fetch credentials for the HFT private NATS stream.
Returns {"api_key": ..., "expires": ..., "signature": ...}.
Pass these directly to the WebSocket auth message;
do not log or persist them.
"""
params = {"expires_in_seconds": expires_in_seconds}
headers, path = sign_request(
"GET", "/dma/api/v1/socket/signature", params)
r = requests.get(BASE_URL + path, headers=headers)
r.raise_for_status()
return r.json()
/** Fetch credentials for the HFT private NATS stream. */
public Map<String, Object> getSocketCredentials(long expiresInSeconds)
throws Exception {
Map<String, String> q = new HashMap<>();
q.put("expires_in_seconds", String.valueOf(expiresInSeconds));
HttpResponse<String> r = send(
"GET", "/dma/api/v1/socket/signature", q, null);
return json.readValue(r.body(), Map.class);
// {"api_key": ..., "expires": ..., "signature": ...}
}
import (
"encoding/json"
"io"
"net/http"
)
// GetSocketCredentials fetches credentials for the HFT private NATS
// stream. Pass these directly to the WebSocket auth message —
// do not log or persist them.
func GetSocketCredentials(expiresInSeconds int) (map[string]any, error) {
params := map[string]string{
"expires_in_seconds": strconv.Itoa(expiresInSeconds),
}
headers, path, err := SignRequest("GET", "/dma/api/v1/socket/signature", params)
if err != nil { return nil, err }
req, _ := http.NewRequest("GET", BaseURL+path, nil)
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var out map[string]any
if err := json.Unmarshal(body, &out); err != nil {
return nil, err
}
return out, nil
}
// Requires signRequest from reference-client.js.
async function getSocketCredentials(expiresInSeconds = 300) {
const {headers, path} = signRequest(
'GET',
'/dma/api/v1/socket/signature',
{expires_in_seconds: expiresInSeconds},
);
const r = await fetch(BASE_URL + path, {headers});
if (!r.ok) throw new Error(`socket sig fetch failed: ${r.status}`);
return r.json();
// {api_key: ..., expires: ..., signature: ...}
}
The returned signature is computed on our side — you don't compute it yourself; just pass it through to the WebSocket auth message. See HFT › Private Streams for how to use the result on the WebSocket.