Search code examples
php

OKX v5 API How to sign a request in PHP


I'm trying to sign an API request to OKX but have been getting "Invalid sign" responses. This is hard to debug, so hopefully someone has done this before. This is what I have:

// AUTHENTICATION https://www.okx.com/docs-v5/en/#rest-api-authentication

function api_request( $endpoint, $parameters ) {

    $timestamp = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );
    $timestamp = $timestamp->format( 'Y-m-d\TH:i:s.v\Z' );
    $header = array( );

    $passphrase = 'xxx';
    $apikey = 'xxx';
    $secretkey = 'xxx';
    $url = 'https://aws.okx.com';
    
    // build request path

    $requestpath = '/api/v5/' . $endpoint;
    if( count( $parameters ) > 0 ) {
        $parameters = http_build_query( $parameters ); // query string encode the parameters
        $requestpath .= '?' . $parameters;    
    }

    // build header
    
    $signed = base64_encode( hash_hmac( 'sha256', $timestamp . 'GET' . $requestpath, $secretkey ) );

    $header[] = 'OK-ACCESS-KEY: ' . $apikey;
    $header[] = 'OK-ACCESS-SIGN: ' . $signed;
    $header[] = 'OK-ACCESS-TIMESTAMP: ' . $timestamp;
    $header[] = 'OK-ACCESS-PASSPHRASE: ' . $passphrase;
    
    // send request and receive response

    $curl = curl_init(); // Get cURL resource
    curl_setopt_array( $curl, array( // Set cURL options
        CURLOPT_HTTPHEADER => $header,      // set the header
        CURLOPT_URL => $url . $requestpath,        // set the request URL
        CURLOPT_RETURNTRANSFER => 1         // ask for raw response instead of bool
    ) );

    $response = curl_exec($curl); // Send the request, save the response
    curl_close($curl); // Close request
    
    $response = json_decode( $response, true ); // Convert to array
    
    return $response;
    
}

// CHECK BALANCE https://www.okx.com/docs-v5/en/#rest-api-funding-get-balance

$a = api_request( 'account/balance', array() );
echo( '<pre>' ); var_dump( $a ); echo( '</pre>' );

This is what I get:

array(2) {
  ["msg"]=>
  string(12) "Invalid Sign"
  ["code"]=>
  string(5) "50113"
}

I tried including the base URL in $requestpath, with or without query parameters, etc. I am also not very familiar with hashing, I have seen python scripts attempting to do this very same thing that will hash it with something like: hash_hmac( 'sha256', bytes( $message, 'utf8' ), bytes( $secret, 'utf8' ), not sure if I should be using something like this in PHP as well.


Solution

  • You have one small bug in creating your signature. You should add a fourth parameter true to the hash_hmac function to return the raw binary format.

    So, your final code for the signature should look like this:

    $signed = base64_encode(hash_hmac('sha256', $timestamp . 'GET' . $requestpath, $secretkey, true));
    

    Final working code:

    <?php
    
    // AUTHENTICATION https://www.okx.com/docs-v5/en/#rest-api-authentication
    
    function api_request( $endpoint, $parameters ) {
    
        $timestamp = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );
        $timestamp = $timestamp->format( 'Y-m-d\TH:i:s.v\Z' );
        $header = array( );
    
        $passphrase = 'xxx';
        $apikey = 'xxx';
        $secretkey = 'xxx';
        $url = 'https://aws.okx.com';
    
        // build request path
    
        $requestpath = '/api/v5/' . $endpoint;
        if( count( $parameters ) > 0 ) {
            $parameters = http_build_query( $parameters ); // query string encode the parameters
            $requestpath .= '?' . $parameters;
        }
    
        // build header
    
        $signed = base64_encode( hash_hmac( 'sha256', $timestamp . 'GET' . $requestpath, $secretkey, true ) );
    
        $header[] = 'OK-ACCESS-KEY: ' . $apikey;
        $header[] = 'OK-ACCESS-SIGN: ' . $signed;
        $header[] = 'OK-ACCESS-TIMESTAMP: ' . $timestamp;
        $header[] = 'OK-ACCESS-PASSPHRASE: ' . $passphrase;
    
        // send request and receive response
    
        $curl = curl_init(); // Get cURL resource
        curl_setopt_array( $curl, array( // Set cURL options
            CURLOPT_HTTPHEADER => $header,      // set the header
            CURLOPT_URL => $url . $requestpath,        // set the request URL
            CURLOPT_RETURNTRANSFER => 1         // ask for raw response instead of bool
        ) );
    
        $response = curl_exec($curl); // Send the request, save the response
        curl_close($curl); // Close request
    
        $response = json_decode( $response, true ); // Convert to array
    
        return $response;
    
    }
    
    // CHECK BALANCE https://www.okx.com/docs-v5/en/#rest-api-funding-get-balance
    
    $a = api_request( 'account/balance', array() );
    echo( '<pre>' ); var_dump( $a ); echo( '</pre>' );
    

    Tested with 3 endpoints: account/balance account/interest-rate account/trade-fee

    enter image description here

    enter image description here

    enter image description here

    UPDATED: Added code below that works for both request types (GET and POST):

    <?php
    
    // AUTHENTICATION https://www.okx.com/docs-v5/en/#rest-api-authentication
    
    function api_request( $endpoint, $type = 'GET', $parameters = []) {
    
        $timestamp = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );
        $timestamp = $timestamp->format( 'Y-m-d\TH:i:s.v\Z' );
        $header = array( );
    
        $passphrase = 'xxx';
        $apikey = 'xxx';
        $secretkey = 'xxx';
        $url = 'https://aws.okx.com';
    
        // build request path
    
        $requestpath = '/api/v5/' . $endpoint;
        $body = '';
        if( count( $parameters ) > 0) {
            if ($type === 'GET') {
                $parameters = http_build_query($parameters); // query string encode the parameters
                $requestpath .= '?' . $parameters;
            } else {
                $body = json_encode($parameters);
            }
        }
    
        // build header
    
        $signed = base64_encode( hash_hmac( 'sha256', $timestamp . $type . $requestpath . $body, $secretkey, true ) );
    
        $header[] = 'Content-Type: application/json';
        $header[] = 'OK-ACCESS-KEY: ' . $apikey;
        $header[] = 'OK-ACCESS-SIGN: ' . $signed;
        $header[] = 'OK-ACCESS-TIMESTAMP: ' . $timestamp;
        $header[] = 'OK-ACCESS-PASSPHRASE: ' . $passphrase;
    
        // send request and receive response
    
        $curl = curl_init(); // Get cURL resource
        curl_setopt_array( $curl, array( // Set cURL options
            CURLOPT_HTTPHEADER => $header,      // set the header
            CURLOPT_URL => $url . $requestpath,        // set the request URL
            CURLOPT_RETURNTRANSFER => 1         // ask for raw response instead of bool
        ) );
        if ($type === 'POST') {
            curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
        }
        $response = curl_exec($curl); // Send the request, save the response
        curl_close($curl); // Close request
    
        $response = json_decode( $response, true ); // Convert to array
    
        return $response;
    
    }
    
    // CHECK BALANCE https://www.okx.com/docs-v5/en/#rest-api-funding-get-balance
    
    $a = api_request( 'trade/order', 'POST', array(
        'instId' => 'BTC-USDT',
        'tdMode' => 'cash',
        'side' => 'buy',
        'ordType' => 'limit',
        'px' => '1000',
        'sz' => '0.01'
    ) );
    
    echo( '<pre>' ); var_dump( $a ); echo( '</pre>' );
    

    enter image description here

    If you want to test your code in the DEMO trading environment, don't forget to add a custom header: $header[] = 'x-simulated-trading: 1';