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#
-
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
{}ornull. - 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
- Hash that entire string with SHA-256, then sign the resulting digest with your secp256k1 private key.
- 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 .