Scroll to top

Pay with Bitcoin using Coinbase and Bitpay

Hi ,
Today we will learn how to integrate payment with bitcoins , we will integrate coinbase and bitpay to pay with bitcoins .

Setting up Bitpay

Register for a new account on bitpay , once it is done login to your account and click on My Account which is under My Information section , Now click on API Access keys and generate a new key using Add New API Key button . Keep your api access key safe , If your key is compromised then anyone can access your account .

This key will be used to authenticate with bitpay and access api points . You can look at bitpay api or download api documentation to understand how it works . They also provide official php library which is hosted on github . We wont be using php client we will access bitpay using basic auth and rest api .

Setting up Coinbase

Register for a new account on coinbase , once it is done login to your account and click on Settings from left sidebar , Now click on API Access and generate a new key using New API Key button , select accounts which have access using this api key and select which permissions you need . Keep your api key and api secret safe , If your key is compromised then anyone can access your account . Coinbase have two factor authentication due to which you have to enter token which is sent on mobile as sms to login or change some of the settings . If you even want to see api keys you need to go through two factor authentication which is highly secure .

This key + secret will be used to authenticate with coinbase and access api . You can look at coinbase api to understand how it works . They also provide official php library which is hosted on github . We wont be using php client we will access coinbase using API key and secret . To create invoice using api you need to complete your merchant profile details and have it verified .

Migrations

Setup your database config in app/database.php after that we will create two tables , first one to store billing details and second to store order information , our migrations are below .

php artisan migrate:make billing_table
 
php artisan migrate:make orders_table
<?php
 
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class BillingTable extends Migration {
 
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('billing', function(Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('address');
            $table->string('city');
            $table->string('state');
            $table->string('zip');
            $table->string('email');
            $table->string('phone');
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists("billing");
    }
 
}
<?php
 
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class OrdersTable extends Migration {
 
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('orders', function(Blueprint $table) {
            $table->increments('id');
            $table->integer('billing_id');
            $table->string('amount');
            $table->string('type');
            $table->string('status');
            $table->text('response');
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('orders');
    }
 
}
php artisan migrate

This will execute our migrations and creates our table schema’s

Views and Routes

We need a basic view where user can enter details like amount and billing information and select checkout type which can be coinbase or bitpay .
Create a new route which will display our index page

Route::get('/','HomeController@index');
Route::post('/process', 'HomeController@processPayment');
Route::post('/notify/{bitcoin_service}', 'HomeController@notify');

In the above code we have created route to index which will display our page , a post route which will handle payment and a notify route which will handle notifications from bitpay and coinbase regarding status change of our invoice .

Our index.blade.php is below

<!DOCTYPE html>
<html>
 
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <style type="text/css">@-ms-viewport{width: device-width;}</style>
    <title>KodeInfo - Bitcoin Payment Integration</title>
    {{HTML::style('//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css')}}
</head>
 
<body>
 
<div class="container">
    <div class='row'>
        <div class='col-md-4'></div>
        <div class='col-md-4'>
          {{Form::open(['url'=>'/process','method'=>'POST'])}}
            <div class='form-row'>
 
              @include('notify')
 
              <div class='col-xs-12 form-group'>
                <label class='control-label'>Amount</label>
                <input class='form-control' name="amount" value="{{Input::old('amount')}}" type='text'>
              </div>
 
            </div>
 
            <div class='form-row'>
              <div class='col-xs-12 form-group'>
                <label class='control-label'>Billing Name</label>
                <input class='form-control' name="name" value="{{Input::old('name')}}" type='text'>
              </div>
            </div>
 
            <div class='form-row'>
              <div class='form-row'>
                <div class='col-xs-6 form-group'>
                    <label class='control-label'>Billing Address</label>
                    <input class='form-control' name="address" value="{{Input::old('address')}}" type='text'>
                </div>
              </div>
              <div class='form-row'>
                <div class='col-xs-6 form-group'>
                    <label class='control-label'>Billing City</label>
                    <input class='form-control' name="city" value="{{Input::old('city')}}" type='text'>
                </div>
              </div>
            </div>
 
            <div class='form-row'>
                <div class='form-row'>
                    <div class='col-xs-6 form-group'>
                        <label class='control-label'>Billing State</label>
                        <input class='form-control' name="state" value="{{Input::old('state')}}" type='text'>
                    </div>
                </div>
 
                <div class='form-row'>
                    <div class='col-xs-6 form-group'>
                        <label class='control-label'>Billing Zip</label>
                        <input class='form-control' name="zip" value="{{Input::old('zip')}}" type='text'>
                  </div>
               </div>
            </div>
 
            <div class='form-row'>
              <div class='col-xs-12 form-group'>
                <label class='control-label'>Billing Email</label>
                <input class='form-control' name="email" value="{{Input::old('email')}}" type='text'>
              </div>
            </div>
            <div class='form-row'>
              <div class='col-xs-12 form-group'>
                <label class='control-label'>Billing Phone</label>
                <input class='form-control' name="phone" value="{{Input::old('phone')}}" type='text'>
              </div>
            </div>
 
            <div class='form-row'>
              <div class='col-xs-12 form-group'>
                <label class='control-label'>Pay with</label>
                <select class='form-control' name="type" >
                    <option value="coinbase">Coinbase</option>
                    <option value="bitpay">Bitpay</option>
                </select>
              </div>
            </div>
 
            <div class='form-row'>
              <div class='col-md-12 form-group'>
                <button class='form-control btn btn-primary submit-button' type='submit'>Submit »</button>
              </div>
            </div>
 
          {{Form::close()}}
        </div>
        <div class='col-md-4'></div>
    </div>
</div>
 
{{HTML::script('//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js')}}
 
</body>
 
 
 
</html>

Our form action is /process route and method is POST .
We also have notify.blade.php which is included in our index. It checks for errors and display on index page .

Config File

Create a new file bitcoins.php inside app/config , this will store our basic information like url , api key , api secret etc  .

<?php
 return [
     'bitpay_url'     => 'https://bitpay.com/api/invoice/',
     'bitpay_api_key'  => '',
     'bitpay_notify_url' => URL::to('/')."/notify/bitpay",
     'coinbase_url'     => 'https://coinbase.com/api/v1/buttons',
     'coinbase_api_key'  => '',
     'coinbase_api_secret'  => '',
     'coinbase_notify_url' => URL::to('/')."/notify/coinbase",
 ];

Insert your bitpay_api_key , coinbase_api_key , coinbase_api_secret in our above config file

Models

Create two eloquent models , one for billing and other for orders .

<?php
 
class Billing extends Eloquent {
 
    protected $table = "billing";
    public $timestamps = true;
 
}
<?php
 
class Order extends Eloquent {
 
    protected $table = "orders";
    public $timestamps = true;
 
}

Integrating Bitpay

The /process route will execute processPayment method from HomeController , Our processPayment method will validate inputs , setup billing object , create an order object and then it will check which type of service needs to be executed . If your have selected bitpay then it will execute bitpayRequestCurl and pass order id , amount , billing object .
If there is any exception in bitpayRequestCurl it will go back to index page with inputs and error messages .

public function processPayment()
    {
        $v = Validator::make(["amount"=>Input::get("amount"),"email"=>Input::get("email")],
            ["amount"=>"required|integer","email"=>"required|email"]);
 
        if($v->passes()){
 
            $billing = new Billing();
            $billing->name = Input::get("name");
            $billing->address = Input::get("address");
            $billing->city = Input::get("city");
            $billing->state = Input::get("state");
            $billing->zip = Input::get("zip");
            $billing->email = Input::get("email");
            $billing->phone = Input::get("phone");
            $billing->save();
 
            $order = new Order();
            $order->amount = Input::get("amount");
            $order->billing_id = $billing->id;
            $order->save();
 
            $bitcoinRedirectURL = URL::to("/");
 
            if(Input::get('type')=="bitpay"){
 
                try {
                    $bitpayResponse = $this->bitpayRequestCurl($order->id,$order->amount,$billing);
                } catch (\Exception $e) {
                    Session::flash("error_msg",$e->getMessage());
                    return Redirect::back();
                }
 
                //Set order status to pending since user didnt paid yet and serialize the response maybe useful later
                $order->type = "bitpay";
                $order->status = "Pending";
                $order->response = serialize($bitpayResponse);
 
                $bitcoinRedirectURL = $bitpayResponse->url;
 
            }
 
            if(Input::get('type')=="coinbase"){
 
                try {
                    $coinbaseResponse =  $this->coinbaseRequestCurl($order->id,$order->amount);
                } catch (\Exception $e) {
                    Session::flash("error_msg",$e->getMessage());
                    return Redirect::back();
                }
 
                $order->type = "coinbase";
                $order->status = "Pending";
                $order->response = serialize($coinbaseResponse);
 
                $bitcoinRedirectURL = "https://www.coinbase.com/checkouts/".$coinbaseResponse->button->code;
 
            }
 
            return Redirect::to($bitcoinRedirectURL);
 
        }else{
 
            $response = "";
            $messages = $v->messages()->all();
 
            foreach($messages as $message){
                $response.="<li style='margin-left:10px;'>{$message}</li>";
            }
 
            Session::flash("error_msg",$response);
            return Redirect::back()->withInput();
        }
 
    }

Our bitpayRequestCurl is using curl to access bitpay through basic authentication . We will pass our json encoded options to post fields using CURLOPT_POSTFIELDS . We will set CURLOPT_HTTPAUTH to CURLAUTH_BASIC . If the response is a success we will decode and return it back else throw an exception .

public function bitpayRequestCurl($orderId, $grandTotal,$billing) {
 
        $config = Config::get('bitcoins');
 
        $url = $config['bitpay_url'];
        $apiKey = $config['bitpay_api_key'];
 
        $post = array(
            'orderID' =>$orderId,
            'price' => $grandTotal,
            'currency' => 'USD',
            'itemDesc' => 'KodeInfo Checkout Invoice',
            'notificationEmail'=>'developers@kodeinfo.com',
            'notificationURL'=>$config['bitpay_notify_url'],
            'physical'=>true,
            'fullNotifications'=>true,
            'buyerName'=>$billing->name,
            'buyerAddress1'=>$billing->address,
            'buyerAddress2'=>'',
            'buyerCity'=>$billing->city,
            'buyerState'=>$billing->state,
            'buyerZip'=>$billing->zip,
            'buyerEmail'=>$billing->email,
            'buyerPhone'=>$billing->phone
        );
 
        $post = json_encode($post);
 
        if((isset($url) && trim($url) != '') && (isset($apiKey) && trim($apiKey) != '')) {
            try {
                $curl = curl_init();
 
                curl_setopt($curl, CURLOPT_POST, 1);
                curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
                $length = strlen($post);
 
                $uname = base64_encode($apiKey);
 
                if($uname) {
                    $header = array(
                        'Content-Type: application/json',
                        'Content-Length: ' . $length,
                        'Authorization: Basic ' . $uname,
                        'X-BitPay-Plugin-Info: phplib1.5',
                    );
 
                    curl_setopt($curl, CURLOPT_URL, $url);
                    curl_setopt($curl, CURLOPT_PORT, 443);
                    curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
                    curl_setopt($curl, CURLOPT_TIMEOUT, 10);
                    curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC ) ;
                    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); // verify certificate
                    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); // check existence of CN and verify that it matches hostname
                    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
                    curl_setopt($curl, CURLOPT_FORBID_REUSE, 1);
                    curl_setopt($curl, CURLOPT_FRESH_CONNECT, 1);
 
                    $responseString = curl_exec($curl);
 
                    if($responseString == false) {
                        throw new \Exception('Error: ' . curl_error($curl));
                    } else {
                        if (!json_decode($responseString, true)) {
                            throw new \Exception('Error - Invalid JSON: ' . $responseString);
                        }
                    }
 
                    curl_close($curl);
 
                    return json_decode($responseString);
                } else {
                    curl_close($curl);
                    throw new \Exception('Invalid data found in apiKey value passed to bitpayRequestCurl. (Failed: base64_encode(apikey))');
                }
            } catch (Exception $e) {
                @curl_close($curl);
                throw new \Exception('Error: ' . $e->getMessage());
            }
        } else {
            throw new \Exception('Error: You must supply non-empty url and apiKey parameters.');
        }
 
    }

If your server do not support curl then you can use bitpayRequest method which does the same thing If the response is a success then decode and return it or else throw an exception .

//You can also use this method which works without Curl
    public function bitpayRequest($orderId,$grandTotal,$billing){
 
        $config = Config::get('bitcoins');
 
        $url = $config['bitpay_url'];
        $apiKey = $config['bitpay_api_key'];
 
        $post = array(
            'orderID' =>$orderId,
            'price' => $grandTotal,
            'currency' => 'USD',
            'itemDesc' => 'KodeInfo Checkout Invoice',
            'notificationEmail'=>'developers@kodeinfo.com',
            'notificationURL'=>$config['bitpay_notify_url'],
            'physical'=>true,
            'fullNotifications'=>true,
            'buyerName'=>$billing->name,
            'buyerAddress1'=>$billing->address,
            'buyerAddress2'=>'',
            'buyerCity'=>$billing->city,
            'buyerState'=>$billing->state,
            'buyerZip'=>$billing->zip,
            'buyerEmail'=>$billing->email,
            'buyerPhone'=>$billing->phone
        );
 
        $post = json_encode($post);
        $length = strlen($post);
        $basic_auth = base64_encode($apiKey);
 
        $context_options = array(
            "http" => array(
                "method" => "POST",
                "header"  => "Content-type: application/json\r\n" .
                    "Content-Length: $length\r\n" .
                    "Authorization: Basic $basic_auth\r\n"
            )
        );
 
        $context_options["http"]["content"] = $post;
        $context = stream_context_create($context_options);
        $response = file_get_contents($url, false, $context);
 
        $decodeResponse = json_decode($response);
 
        if(empty($decodeResponse)){
            Log::alert(Request::all());
            throw new \Exception("Unable to process payment contact customer support");
        }else{
            return $decodeResponse;
        }
 
    }

Integrating Coinbase

The /process route will execute processPayment method from HomeController , Our processPayment method will validate inputs , setup billing object , create an order object and then it will check which type of service needs to be executed . If your have selected coinbase then it will execute coinbaseRequestCurl and pass order id , price string .
Below is the example request for coinbase

GET /api/v1/account/balance HTTP/1.1
Accept: */*
User-Agent: Ruby
ACCESS_KEY: <YOUR-API-KEY>
ACCESS_SIGNATURE: <YOUR-COMPUTED-SIGNATURE>
ACCESS_NONCE: <YOUR-UPDATED-NONCE>
Connection: close
Host: coinbase.com

Response:

{"amount"=>"50.00000000", "currency"=>"BTC"}

The ACCESS_KEY header is simply your API key.
The ACCESS_SIGNATURE header is a HMAC-SHA256 hash of the nonce concatentated with the full URL and body of the HTTP request, encoded using your API secret.
The nonce is a positive integer number that must increase with every request you make. The nonce parameter can be included in one of three ways:

As a ACCESS_NONCE header in your requests

ACCESS_NONCE: 1234

As a GET parameter in the URL

https://api.coinbase.com/v1/account/balance?nonce=1234

As a root-level parameter in your JSON POST requests
This scheme is generally known as “HMAC authentication” (see wikipedia article).

public function coinbaseRequestCurl($orderId,$priceString){
 
       $config = Config::get('bitcoins');
       $apikey = $config["coinbase_api_key"];
       $apisecret = $config["coinbase_api_secret"];
       $nonce = sprintf('%0.0f',round(microtime(true) * 1000000));
 
       $url = $config['coinbase_url']."?nonce=" . $nonce;
 
       $parameters = [];
       $parameters["button"]["name"] = "KodeInfo Checkout Invoice";
       $parameters["button"]["custom"] = $orderId;
       $parameters["button"]["price_string"] = $priceString;
       $parameters["button"]["type"] = "buy_now";
       $parameters["button"]["subscription"] = false;
       $parameters["button"]["price_currency_iso"] = "USD";
       $parameters["button"]["description"] = "KodeInfo Checkout Invoice";
       $parameters["button"]["style"] = "custom_large";
       $parameters["button"]["include_email"] = true;
       $parameters["button"]["callback_url"] = $config['coinbase_notify_url'];
       $parameters = http_build_query($parameters, true);
 
       $signature = hash_hmac("sha256", $nonce . $url . $parameters, $apisecret);
 
       $ch = curl_init();
 
       curl_setopt_array($ch, array(
           CURLOPT_URL => $url,
           CURLOPT_RETURNTRANSFER => true,
           CURLOPT_HTTPHEADER => array(
               "ACCESS_KEY: " . $apikey,
               "ACCESS_NONCE: " . $nonce,
               "ACCESS_SIGNATURE: " . $signature
           )));
 
       curl_setopt_array($ch, array(
           CURLOPT_POSTFIELDS => $parameters,
           CURLOPT_POST => true,
       ));
 
       $response = curl_exec($ch);
       curl_close($ch);
 
       $decodeResponse = json_decode($response);
 
       if(sizeof($decodeResponse->errors)>0){
           Log::alert(Request::all());
           throw new \Exception($decodeResponse->errors[0]);
       }else{
           return $decodeResponse;
       }
   }

Response from coinbase will contain code which need to be appended to the below url and redirect so the user can complete the payment.

Thanks
KodeInfo

Author avatar

admin

http://www.kodeinfo.com
Imran is a web developer and consultant from India. He is the founder of KodeInfo, the PHP and Laravel Community . In the meantime he follows other projects, works as a freelance backend consultant for PHP applications and studies IT Engineering . He loves to learn new things, not only about PHP or development but everything.

Post a Comment

Your email address will not be published. Required fields are marked *