Authentication and request signing

Register your public key, then sign each request with its matching private key.

Before your first request

You need an enabled Coinhako API account, a registered secp256k1 public key, and the matching private key. Contact your Coinhako representative or hello@coinhako.com to arrange access.

Tell Coinhako which environment and account you will use, which permissions you need, and your service’s outbound IP addresses. A key created locally will not authenticate until Coinhako registers it against the correct account and environment.

Access What to arrange
Read access A registered key for viewing account data.
Trading The trade scope for quotes, order creation, and cancellation. Provide your outbound IPs for allowlisting.
Transfers The transfer scope for withdrawals. Provide your outbound IPs for allowlisting.

Scopes and IP allowlists belong to the API key. They are separate from account readiness: trading and withdrawals also depend on account eligibility, limits, enabled features, and sufficient funds. Set up authenticator-app 2FA (TOTP) through the Coinhako app before trading. See trading and withdrawals for their prerequisites.

Generate your key pair

Create a private key with OpenSSL:

umask 077
openssl ecparam -genkey -name secp256k1 -noout -out my_private_key.pem

Extract the uncompressed public key as uppercase hexadecimal with Ruby’s standard OpenSSL library:

ruby -ropenssl - my_private_key.pem <<'RUBY'
key = OpenSSL::PKey.read(File.read(ARGV.fetch(0)))
puts key.public_key.to_bn(:uncompressed).to_s(16).upcase
RUBY

The public key has 130 hexadecimal characters and starts with 04. Send only the public key to Coinhako for registration. Keep the PEM private key in your own secret storage; do not put it in request headers, source control, or shared files.

Required headers

Include all four headers on authenticated requests:

Header Value
X-API-KEY Your registered public key, in hexadecimal. Use the exact text registered with Coinhako, including letter case.
X-API-Algorithm ecdsa-secp256k1-sha256
X-API-Timestamp Current Unix time in milliseconds, as a decimal string.
X-API-Signature DER-encoded ECDSA signature, expressed as uppercase hexadecimal.

Generate a fresh timestamp and signature immediately before sending each request. The timestamp must be within one minute of the server’s time; keep your system clock synchronized. A seconds-based timestamp will fail.

Build the signature

  1. Hash the exact bytes of the HTTP request body with SHA-256. Encode this digest as uppercase hexadecimal. For a request without a body, hash the empty string, not {} or null.
  2. Join these five fields in this order, with & separators and no added spaces or newline:
method=<METHOD>&target-uri=<FULL_URL>&content-digest=<BODY_SHA256_HEX>&created=<TIMESTAMP_MS>&alg=ecdsa-secp256k1-sha256
  1. Hash that entire string with SHA-256, then sign the resulting digest with your secp256k1 private key.
  2. Encode the signature as ASN.1 DER, then uppercase hexadecimal, and send it in X-API-Signature.
Signed field Match it to
method Uppercase HTTP method, such as GET or POST.
target-uri Full HTTPS URL, including hostname, path, and query string. Preserve query ordering and URL encoding.
content-digest The body bytes you actually send, including whitespace and any trailing newline.
created Exactly the same value as X-API-Timestamp.
alg Exactly ecdsa-secp256k1-sha256.

For production, use https://api.coinhako.com/v1 as the base URL. Use the environment URL supplied during onboarding for testing. Changing the hostname, query, body formatting, or timestamp after signing invalidates the signature. HTTP redirects can also change the signed URL; send directly to the intended endpoint.

Paths on api.coinhako.com start with /v1. The API reference uses the corresponding /public_api/v1 paths on www.coinhako.com. Sign the full URL you actually send, including the hostname, path, and query string. The legacy API-host signing code removes that prefix when constructing its signing input.

The SHA-256 digest of an empty body is:

E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855

Ruby signing example

This uses Ruby’s standard libraries and makes a read-only balances request. dsa_sign_asn1 signs the digest directly; if another library hashes its input internally, give that library the signature-input string so it hashes it once.

require 'openssl'
require 'net/http'
require 'uri'

key = OpenSSL::PKey.read(File.read(ENV.fetch('COINHAKO_PRIVATE_KEY_PATH')))
base_url = ENV.fetch('COINHAKO_API_BASE_URL', 'https://api.coinhako.com/v1')
uri = URI("#{base_url}/accounts/balances?currency=BTC")
method = 'GET'
body = ''
timestamp = (Time.now.to_r * 1000).to_i.to_s
algorithm = 'ecdsa-secp256k1-sha256'
digest = OpenSSL::Digest::SHA256.hexdigest(body).upcase

input = [
  "method=#{method}",
  "target-uri=#{uri}",
  "content-digest=#{digest}",
  "created=#{timestamp}",
  "alg=#{algorithm}"
].join('&')
signature = key.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(input)).unpack1('H*').upcase

request = Net::HTTP::Get.new(uri)
request['X-API-KEY'] = key.public_key.to_bn(:uncompressed).to_s(16).upcase
request['X-API-Algorithm'] = algorithm
request['X-API-Timestamp'] = timestamp
request['X-API-Signature'] = signature

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code
puts response.body

For JSON requests, serialize once, compute the digest from that string, and send the same string as the request body. Do not reformat it between signing and sending.

Continue with the quickstart . If authentication fails, use the error checklist .

Search guides

Search the guides. Use the API reference search for endpoints.