Certain Anchorage endpoints require an Ed25519 signature in the request headers alongside the API key.
Signatures are optional unless explicitly required, but are encouraged for all requests for maximum security. The Api-Signature is valid for 60 seconds.
Create the signature by concatenating these values:
timestamp + uppercase HTTP method + request path including query + request body
For requests without a body, omit the body from the signature input. For
requests with a JSON body, sign the exact bytes sent over the wire —
that is, JSON.stringify(body) with no extra whitespace. Any difference
(stray spaces, trailing newlines, key reordering) will produce a signature
the server rejects.
| Value | Notes |
|---|
timestamp | Seconds since the Unix Epoch in UTC. It must be within one minute of the API service time. |
method | Uppercase HTTP method, such as GET, POST, or DELETE. |
request path | Path and query string, such as /v2/transfers?foo=bar&baz=bang. |
body | JSON.stringify(body) for requests that include one. Empty for GET. |
Sign a request
Java
C#
Ruby
Python
Node.js
package com.anchorage.api.client;
import okio.Buffer;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.Signer;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.json.JSONObject;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
/**
* A sample java API client to connect to Anchorage API v2
*
* Required dependencies:
* org.json:json:20210307
* org.bouncycastle:bcpkix-jdk15on:1.69
* commons-codec:commons-codec:1.15
* com.squareup.okio:okio:1.9.0
*/
public class RestClientWithSigning {
private RestTemplate restTemplate;
public static void main(String[] args){
RestClientWithSigning api = new RestClientWithSigning();
api.init();
api.apiCall();
}
private void init(){
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(2000);
factory.setReadTimeout(2000);
restTemplate = new RestTemplate(factory);
}
private void apiCall() {
try {
String url = "https://api.anchorage-staging.com/v2/trading/quote";
String body = createRequestBody();
HttpEntity<String> request = new HttpEntity<>(body, createHeaders("/v2/trading/quote", HttpMethod.POST, body));
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
if (responseEntity != null && HttpStatus.CREATED == responseEntity.getStatusCode()) {
System.out.println(String.format("Getting data from ( %s ) response: %s", url, responseEntity.getBody()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
private String createRequestBody(){
JSONObject body = new JSONObject();
body.put("currency", "ETH");
body.put("quantity", "1.2");
body.put("side", "BUY");
body.put("tradingPair", "ETH-USD");
return body.toString();
}
private HttpHeaders createHeaders(String requestPath, HttpMethod httpmethod, String body) throws DecoderException, CryptoException {
String api_key = "your API Key";
String signing_key_hex = "Your signing key";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Api-Access-Key", api_key);
String timestamp = String.valueOf(Instant.now().getEpochSecond());
byte[] toSign = decodeMessage(timestamp, httpmethod, requestPath, body);
String signature = bc_sign(Hex.decodeHex(signing_key_hex), toSign);
headers.add("Api-Signature", signature);
headers.add("Api-Timestamp", timestamp);
return headers;
}
private byte[] decodeMessage(String timestamp, HttpMethod httpmethod, String url_path, String body){
Buffer buffer = new Buffer();
buffer.write(timestamp.getBytes(StandardCharsets.UTF_8))
.write(httpmethod.name().getBytes(StandardCharsets.UTF_8))
.write(url_path.getBytes(StandardCharsets.UTF_8))
.write(body.getBytes(StandardCharsets.UTF_8));
return buffer.readByteArray();
}
private String bc_sign(byte[] signing_key, byte[] toSign) throws CryptoException {
Ed25519PrivateKeyParameters privateKeyParameters = new Ed25519PrivateKeyParameters(signing_key);
Signer signer = new Ed25519Signer();
signer.init(true, privateKeyParameters);
signer.update(toSign, 0, toSign.length);
return Hex.encodeHexString(signer.generateSignature());
}
}
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Sodium;
// Dependencies: Sodium.Core 1.3.1
// Replace privateKey, publicKey, and Api-Access-Key with your keys
namespace WebAPIClient
{
class Program
{
private static readonly HttpClient client = new HttpClient();
static async Task Main(string[] args)
{
await ProcessAnchorageAPISign();
}
private static async Task<Object> ProcessAnchorageAPISign()
{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
string privateKey = "your private key here";
string publicKey = "your public key here";
string signingKey = $"{privateKey}{publicKey}";
string timestamp = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
string path = "/v2/trading/quote/accept";
string method = "POST";
string payload = "{\"quoteID\":\"1b1748b0-a62a-46c7-802c-7c61ac222424\",\"side\":\"BUY\"}";
string msgToSign = $"{timestamp}{method}{path}{payload}";
var signature = ComputeSignature(Utilities.HexToBinary(signingKey), Encoding.UTF8.GetBytes(msgToSign));
client.DefaultRequestHeaders.Add("Api-Access-Key", "your api access key");
client.DefaultRequestHeaders.Add("Api-Timestamp", timestamp);
client.DefaultRequestHeaders.Add("Api-Signature", signature);
Uri u = new Uri("https://api.anchorage-staging.com/v2/trading/quote/accept");
HttpContent c = new StringContent(payload, Encoding.UTF8, "application/json");
HttpResponseMessage result = await client.PostAsync(u, c);
var response = await result.Content.ReadAsStringAsync();
Console.WriteLine(response);
return response;
}
static string ComputeSignature(byte[] privateKey, byte[] toSign)
{
byte[] signature = PublicKeyAuth.SignDetached(toSign, privateKey);
return Utilities.BinaryToHex(signature);
}
}
}
require "ed25519"
require "net/http"
require "time"
def hex_to_bin(s)
[s].pack('H*')
end
def bin_to_hex(s)
s.unpack('H*').first
end
private_key_seed_hex = '0101010101010101010101010101010101010101010101010101010101010101'
public_key_hex = '8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c'
key_pair_hex = private_key_seed_hex + public_key_hex
key_pair = hex_to_bin(key_pair_hex)
signing_key = Ed25519::SigningKey.from_keypair(key_pair)
timestamp = '1577880000' # Time.now.to_i.to_s
req = Net::HTTP::Post.new('/v2/transfers?foo=bar&baz=bang')
req.body = '{"source": {"id": "1c920f4241b78a1d483a29f3c24b6c4c", "type": "VAULT"},
"assetType": "ETH", "destination": {"id": "55e89d4a644d736b01533a2ea9b32a20", "type": "VAULT"}, "amount": "1000.00000000"}'
signature = signing_key.sign(timestamp + req.method + req.path + req.body)
req['Api-Access-Key'] = 'YOUR_ACCESS_KEY'
req['Api-Timestamp'] = timestamp
req['Api-Signature'] = bin_to_hex(signature)
puts bin_to_hex(signature)
import time
import requests
from nacl import signing
class AnchorageAuth(requests.auth.AuthBase):
ACCESS_KEY_HEADER = "Api-Access-Key"
SIGNATURE_HEADER = "Api-Signature"
TIMESTAMP_HEADER = "Api-Timestamp"
def __init__(self, access_key: str, signing_key_seed: bytes):
self.access_key = access_key
self.signing_key = signing.SigningKey(signing_key_seed)
def __call__(self, request: requests.PreparedRequest):
request.headers[self.ACCESS_KEY_HEADER] = self.access_key
timestamp = str(int(time.time()))
method = request.method.upper() if request.method else "GET"
body = request.body or b""
if isinstance(body, str):
body = body.encode("utf-8")
message = b"".join([
timestamp.encode("utf-8"),
method.encode("utf-8"),
request.path_url.encode("utf-8"),
body,
])
request.headers[self.SIGNATURE_HEADER] = self.signing_key.sign(message).signature.hex()
request.headers[self.TIMESTAMP_HEADER] = timestamp
return request
const { sign } = require("@noble/ed25519");
const axios = require("axios");
class AnchorageClient {
constructor(accessKey, signingKeySeed) {
this.accessKey = accessKey;
this.signingKeySeed = signingKeySeed; // 32-byte hex seed
this.basePath = "https://api.anchorage-staging.com";
}
async sendSignedRequest(method, endpoint, body) {
const timestamp = Math.floor(Date.now() / 1000);
const serializedBody = body ? JSON.stringify(body) : "";
const signatureInput = `${timestamp}${method}${endpoint}${serializedBody}`;
const messageHex = Buffer.from(signatureInput, "utf8").toString("hex");
const signature = await sign(messageHex, this.signingKeySeed);
const signatureHex = Buffer.from(signature).toString("hex");
return axios({
method,
url: this.basePath + endpoint,
data: body,
headers: {
"Api-Access-Key": this.accessKey,
"Api-Signature": signatureHex,
"Api-Timestamp": String(timestamp),
"Content-Type": "application/json",
},
}).then((res) => res.data);
}
}
// Example: request and accept a quote
const client = new AnchorageClient("YOUR_ACCESS_KEY", "YOUR_SIGNING_KEY_SEED");
const quote = await client.sendSignedRequest("POST", "/v2/trading/quote", {
tradingPair: "BTC-USD",
quantity: "1",
currency: "BTC",
side: "BUY",
});
await client.sendSignedRequest("POST", "/v2/trading/quote/accept", {
quoteID: quote.data.quoteID,
side: "BUY",
allowedSlippage: "0.001",
});
JSON.stringify(body) produces the same compact bytes the server signs
against, so you don’t need to pre-serialize the request manually. The live
signer above applies the same canonicalization to anything you paste into
the body field.