HodlHodl API Documentation (v1)
Notice: in order to use API you should enable it in your account preferences.
Authorization
Use API key from your account preferences and add the following header to each request:
Authorization: Bearer <your_api_key>
The next endpoints are public and do not require API key:
-
GET /api/v1/countries
-
GET /api/v1/currencies
-
GET /api/v1/offers
-
GET /api/v1/offers/:id
-
GET /api/v1/payment_methods
“Signed request”
If you want to provide release_address
in Creating contract request, you have to “sign” the request.
To sign the request:
- Get current time as Unix timestamp (integer). Convert that number to a string (using the usual decimal representation, e.g. you should get
"1596026883"
if current time is 2020-07-29 12:48:03 UTC). We’ll refer to that string asnonce
. - Retrieve Signature Key from the user (which could be obtained at “API Access” tab of “Account settings” section of the website when API access is enabled). If you want to save Signature Key for further usage (i.e. to not request user to enter Signature Key each time it’s required) you should explicitly warn the user that that would allow your service/application to directly access user’s funds. We’ll refer to Signature Key as
signature_key
.
Then, calculate the following: SHA256-HMAC(key = signature_key, message = "<api_key>:<nonce>")
. I.e., calculate HMAC:
- Using SHA256 as cryptographic hash function
- Using
signature_key
as secret key - Using the concatenation of the following three strings as message/payload:
<api_key>
(the user’s “regular” API key, see the very top of this document),:
(colon character),<nonce>
(as described above in this section)
We’ll call the resultant value of this calculation hmac
. Represent/digest hmac
as hex string (use digits and lower case only a
-f
symbols).
Then, add nonce
and hmac
parameters to the corresponding API request (to the root of the request parameters).
Each nonce
you provide must be greater (as a number) than the one provided in the previous signed request. Also, nonce
must correspond to a time reasonably close to “the now”.
Notice, that you must not provide signature key as request parameter.
IMPORTANT: “signed request” procedure goal is to enable a user to select desired “trust level” for the service/application which runs API requests on the user’s behalf (by allowing the user to provide only their API key to the service/application, and not their signature key). While using “signed request” it is especially important for a service/application to ensure that direct and proper HTTPS connection with the trading platform’s servers has been established. Signed request is not aimed to reinvent internet security, and shouldn’t be considered as such, thus it fully relies on the underlying HTTPS protocols to ensure safety.
Error handling
The server will respond with JSON with error_code
and relevant HTTP status.
The response is a JSON object (hash) with "status"
key.
"status"
is set to "success"
(if the request was fulfilled) or
"error"
(otherwise).
"error_code"
key contains error code (short string) if "status"
is "error"
.
If the requested entity wasn’t found, the following response
will be sent with 404 NOT FOUND
HTTP status:
{
"status": "error",
"error_code": "not_found"
}
If required root parameter of the request is ommitted, the following response
will be sent with 400 BAD REQUEST
HTTP status:
{
"status": "error",
"error_code": "missing_parameter",
"parameter": "<parameter name here>"
}
If rate limit has been exceeded, the following will be sent with 429 TOO MANY REQUESTS
HTTP status:
{
"status": "error",
"error_code": "rate_limit_exceeded",
"message": "rate limit is 2 per 60s"
}
If service is temporarily unavailable, the following response will be sent with 503 SERVICE UNAVAILABLE
HTTP status:
{
"status": "error",
"error_code": "not_available"
}
(When this reponse is received the request should be repeated later.)
Validation errors
If any validation errors occur when trying to create or update an entity, then the following response will be returned with HTTP status 422
:
{
"status": "error",
"error": "validation",
"validation_errors": {
"<field>": ["<error1>", "<error2>", ...]
}
}
I.e., "validation_errors"
will be returned, which is a hash. Each key of that hash corresponds to the entity’s erroneous field, and the value is an array of human-readable English strings (representing occured validation errors).
Contracts API restrictions
There is no way to create (resolve, work with, etc.) disputes or leave feedback to a counterparty via the API at the moment. Such actions currently should be performed manually via the website.
All contracts operations require payment password to be created via the website (Profile -> Trading Settings). This operation actually creates payment seed on the client (in the browser) and stores encrypted payment seed on the server. Payment seed should be created by offer’s acceptor as well as offer’s creator.
If payment password hasn’t been created on the website, validation error will be returned to “Create contract” request. Currently there is no way to create payment password/encrypted seed via the API for security reasons.
If “Bitcoin address” hasn’t been set in “Trading Settings”, validation error will be returned to “Create contract” request. Currently there is no way to set release address on a per contract basis for security reasons.
Encrypted payment seed could be retrieved via Users/Getting myself method.
Contracts API workflow
- Buyer or seller sends “Creating contract” request to the server
- Initiator sends “Getting contract” request once every 1-3 minutes until
contract.escrow.address
is notnull
(thus, waiting for offer’s creator to confirm their payment password in case they use the website) - Each party verifies the escrow address locally
- Each party sends “Confirming contract’s escrow validity” request to the server
- Seller deposits cryptocurrency to escrow address
- Buyer checks contract status with “Getting contract” request to the server once every 1-5 minutes until contract status is
"in_progress"
- Buyer verifies locally that correct amount of cryptocurrency has been deposited to escrow address and correct number of blockchain confirmations has been received
- Buyer sends fiat payment to the seller
- Buyer sends “Marking contract as paid” request to the server
- Seller verifies locally that they actually has received fiat payment
- Seller fetches pre-signed by the server release transaction with “Showing release transaction of contract” request
- Seller verifies and signs the release transaction locally
- Seller sends the signed release transaction to the server with “Signing release transaction of contract”
If you are the seller, DO NOT SEND BITCOINS UNTIL CONTRACT STATUS IS "depositing"
.
If you are the buyer, DO NOT SEND PAYMENT UNTIL CONTRACT STATUS IS "in_progress"
.
Note: both buyer and seller could check contract status (including information on deposited cryptocurrency) at any time with “Getting contract” request.
Client-side operations
The following operations should be performed on the client (as decribed above):
- seed encryption/decryption
- escrow validation
- transaction signing
References
See HodlHodl Multisig contract specification for detailed description of multisig.
See Bitcoin core documentation for detailed description of P2SH(P2WSH) addresses.
Seed encryption/decryption
The terms “seed” and “xpub” are used as defined in BIP32. Recommended seed length is 64 bytes.
To obtain public or private key for a particular contract use derivation path m/i
, where i
is provided by the server in as contract.escrow.index
in Getting contract response.
Seed is encrypted with so called “payment password” which results in “encrypted seed”.
When user creates payment password on the trading platform’s website what actually happens is:
- browser generates random seed
- browser encrypts this seed with user-provided payment password
- browser sends encrypted seed to the server
- server stores encrypted seed with user data
This workflow allows user to seamlessly change clients without need to export/import their seed (they only need to remember payment password and provide it to the client). Encrypted seed is provided in Getting myself response, clients might save and back up that value for additional safety.
Encrypted seed is a string of the following kind:
ES1:<ciphertext_hex>:<salt_hex>:pbkdf2:10000
ES1
is the version of the algorithm used to encode the string (currently only one version exists), pbkdf2
is the name of key derivation function and 10000
is the number of iterations for PBKDF2. Clients must treat ES1
, pbkdf2
, 10000
as fixed values and check that encrypted seed satisfies this template.
<ciphertext_hex>
is hex-encoded result of AES encryption of the seed.
<salt_hex>
is hex-encoded salt, used for several purposes.
To decrypt seed use the following algorithm (pseudocode):
- Let
payment_password
be payment password of the user - Check that encrypted seed satisfies regexp
/^ES1:([a-f0-9]+):([a-f0-9]+):pbkdf2:10000$/
- Split encrypted seed with ‘:’, extract 2nd element as
ciphertext_hex
, 3rd element assalt_hex
- Let
ciphertext
be the hex-decoded value ofciphertext_hex
,salt
be the hex-decoded value ofsalt_hex
- Derive
aesKey
with PBKDF2-HMAC-SHA256:aesKey = PBKDF2-SHA256(password = payment_password, salt = salt, derived key len = 16 bytes, iterations = 10000)
- Derive
seed
:- Let
iv
be the first 12 bytes ofciphertext
- Let
authTag
be the last 16 bytes ofciphertext
- Let
content
be all the rest ofciphertext
(without first 12 and last 16 bytes) - Perform AES-128-GCM decyption
seed = AES-128-GCM-DECRYPT(key = aesKey, iv = iv, auth tag length = 16 bytes, auth tag = authTag, ciphertext = content)
- If decryption fails, consider
payment_password
to be wrong
- Let
- Verify decrypted seed
- Let
mac
be the first 16 bytes ofSHA256-HMAC(key = payment_password, message = seed)
- Check that
mac
equals tosalt
. If not, considerpayment_password
to be wrong
- Let
Example
Encrypted seed
ES1:7eda9c6f743602a3175d4c522381ab8fb402a75e57cdb3af38aaa0511eac09f28ac45e6f3c368962453de190081f94311fbeab69406e8a36241f0f79bb5d81ee881c73eea3cc488f501cf0030355374f1bba0c16071a773e9e7c1879:8a4acbac1c107faf5652782d5b569a7e:pbkdf2:10000
XPub
xpub661MyMwAqRbcFRUMeZHWy6W9D7MkVbboYehf3RrDPdGFxqfHD3uWBJfMFrrMjTNEddTgYSFktearKrCnoAZV949SSNpakwMGPDeu98DZFiU
Payment password: gg5GvNZAwgWQeVS
aesKey (hex): de9bccdb2a8d14136967bc3286f0ce6a
Decrypted seed (hex)
646131227d65c6d729bc82ddc9e4d141ab69b1ee0dc840cf07183fc8b6f7faeb9ba2d9f0ad357102c578a74700c4491349cd4002991a1c4e8d41accc538d27cc
Derived private key for index 10164 (hex)
4aca31a8e38da930d260253b8c02191abf4bf40bce355712522ac369229b7ea5
Escrow validation
Client must validate witness script and escrow address using information from Getting contract request. Witness script in the response is hex-encoded.
Witness script (contract.escrow.witness_script
)
- Must follow 2-of-3 P2SH(P2WSH) multisig template
- Must contain correct client’s public key as one of 3 public keys (derived from the client’s seed using
contract.escrow.index
as derivation index)
Escrow address (contract.escrow.address
)
- Must be 2-of-3 P2SH(P2WSH) multisig address
- Must use the same three pubkeys which are used in the witness script
Notes
It is recommended to use well-tested libraries to check escrow address.
The following regexp could be used to check witness script hex string and extract public keys:
// javascript example
const twoOfThreeWitness = /^5221([a-f0-9]{66})21([a-f0-9]{66})21([a-f0-9]{66})53ae$/
const script = "522103327a0c6b641047a0a1579fe524c585bbfa79a5eb72c369e4a76f8fb31dbd69ee2102f2f4b089dcfbdf5e129c7747f4a63bfa0c503c0eaf391140998bcd7abafafeab210276f96783752ec81e2afee2b2b24f225ff04174b3d9da220c4b8a2c7a71f8689453ae"
const [_, hodlPubKey, sellerPubKey, buyerPubKey] = script.match(twoOfThreeWitness)
The following code could be used with bitcoinjs-lib to check address:
// javascript - example continues from the previous block of code
import * as bitcoin from 'bitcoinjs-lib'
import {Buffer} from 'buffer'
const correctAddress = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({
m: 2,
pubkeys: [hodlPubKey, sellerPubKey, buyerPubKey].map((x) => Buffer.from(x, 'hex')),
network: bitcoin.networks.bitcoin // or bitcoin.networks.testnet for testnet
})
})
}).address
// compare correctAddress with contract.escrow.address
Transaction signing
Server will return hex-encoded partially-signed transaction (i.e. instead of the user’s signature there would be several 0
symbols) with Showing release transaction of contract or Showing refund transaction of contract methods.
Using HodlHodl Multisig contract specification for reference validate that:
- Transaction inputs are from escrow address
- Transaction outputs contain user’s release (or refund, if this is a refund transaction) address
- User address receives valid amount of funds (escrow amount minus applicable network and service fees)
Then sign the transaction (add user’s signature instead of 0
s in the corresponding place) and post it to the server with Signing release transaction of contract or Signing refund transaction of contract request.
Contract fees breakdown
See Terms of Service for full explanation on how fees work. Below is non-binding explanation for fields seen in API responses (several Contracts and Offers methods) and how they correspond to the websites’ fields.
- | Website text | API field name | Meaning |
---|---|---|---|
1 | How much do you want to buy/sell? (Entered in BTC) | – | Amount of BTC a buyer will receive/seller will give |
2 | How much do you want to buy/sell? (Entered in fiat currency) | contract.value |
Amount of fiat a buyer will pay/seller will receive |
3 | To be deposited (BTC) | contract.volume |
Amount of BTC a seller should pay (blockchain fee paid on top of that) |
4 | Goes to the buyer (BTC) | contract.volume_breakdown.goes_to_buyer |
Amount of BTC a buyer will receive (blockchain fee paid from that) |
5 | Trading fee (BTC) | contract.volume_breakdown.exchange_fee |
Amount of BTC trading platform receives (blockchain fee not applicable) |
6 | Fiat trading fee deduction (fiat) | contract.volume_breakdown.exchange_fee_in_fiat |
Half of amount from previous row (#5) converted to fiat (fiat equivalent of buyer’s part of trading fee) |
7 | Transaction fee (estimated) (BTC) | contract.volume_breakdown.transaction_fee , offer.fee.transaction_fee |
Estimated blockchain fee which a buyer will pay (#4) |
8 | Current trading fee: XX% (percent/fraction) | offer.fee.author_fee_rate , offer.fee.your_fee_rate |
Percent (website) or the same as fraction (API) of trading fee set for particular user. Minimum among “your” (current user) and “author” (offer creator) fee rate (#9) is multiplied by contract volume (#3) and results in trading fee (#5). |
9 | – | offer.fee.exchange_fee |
Minimum among “your” and “author” (#8) trading fee |
Backward-incompatible changes
Earlier there was filters.volume
field in GET /api/v1/offers
and GET /api/v1/offers/fetch_new
, now it is replaced with filters.amount
field (see endpoints’ description for futher details).