Webhooks

A webhook sends a POST request with a charge's data to a URL whenever it's created or its status changes.

Creating webhooks

The way you create webhooks is through your Bidali account Dashboard. Simply select the Developers > Webhooks item from the sidebar menu and you will be presented with this user interface:

Webhooks Dashboard

In order to receive these requests, you should create an endpoint in your server to process it.

Webhook form

You'll need to specify the URL the request will be sent to and a secret. An HMAC signature will be sent in the x-signature header and you'll be able to compute it using the secret.

This allows you to validate that the events were sent by Bidali, not by a third party.

Listening to Webhook events

Creating a Webhook endpoint on your server is really no different from creating any other page on your website. Of course things will be different depending on which language or framework you're building your application with.

Verifying webhooks

Only charges with a statusCode >= 300 and statusCode < 400 are considered successful so you should only mark orders as paid or send out a customers order if a charge has one of those status codes.

Webhooks created through the API are verified by calculating a digital signature. Each webhook request includes a hex-encoded X-Signature header, which is generated using the webhooks shared secret along with the data sent in the request.

Webhooks created through the dashboard are verified using the secret displayed in the Webhooks section of the Dashboard.

To verify that the request came from our API, compute the HMAC digest according to the following algorithm and compare it to the value in the X-Signature header. If they match, then you can be sure that the webhook was sent from us.

Note: If you're using a Rack based framework such as Ruby on Rails or Sinatra, then the header you are looking for is HTTP_X_SIGNATURE.

Here are a few examples:

Ruby
Python w/ Flask
PHP
Node.js w/ Express
require 'rubygems'
require 'openssl'
require 'sinatra'

# Your webhook secret, viewable from the dashboard
WEBHOOK_SECRET = 'YOUR WEBHOOK SECRET'

helpers do
  # Compare the computed HMAC digest based on the shared secret and the request contents
  # to the reported HMAC in the headers
  def verify_webhook(data, hmac_header)
    calculated_hmac = OpenSSL::HMAC.hexdigest('sha1', WEBHOOK_SECRET, data)
    ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
  end
end

# Respond to HTTP POST requests sent to this web service
post '/webhook' do
  request.body.rewind
  data = request.body.read
  verified = verify_webhook(data, env["HTTP_X_SIGNATURE"])

  puts "Webhook verified: #{verified}"
  status 200
  body ''
  json_data = JSON.parse(data)
  status_code = json_data[:data][:statusCode]
  if statusCode >= 300 && statusCode < 400
    # Payment was successful, send out order, mark as paid, etc.
  end
end
from flask import Flask, request, abort
import hmac
import hashlib

app = Flask(__name__)

WEBHOOK_SECRET = 'YOUR WEBHOOK SECRET'

def verify_webhook(data, hmac_header):
    computed_hmac = hmac.new(WEBHOOK_SECRET, data.encode('utf-8'), hashlib.sha1).hexdigest()

    return hmac.compare_digest(computed_hmac, hmac_header.encode('utf-8'))

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    data = request.get_data()
    verified = verify_webhook(data, request.headers.get('X-Signature'))

    if not verified:
        abort(401)

    # process webhook payload
    # ...
    jsonData = request.get_json()
    statusCode = jsonData['data']['statusCode']
    if statusCode >= 300 and statusCode < 400:
        # Payment was successful, send out order, mark as paid, etc
        print('Payment successful')

    return ('Webhook verified', 200)
define('WEBHOOK_SECRET', 'YOUR WEBHOOK SECRET');

function verify_webhook($data, $hmac_header) {
  $calculated_hmac = hex_encode(hash_hmac('sha1', $data, WEBHOOK_SECRET, true));
  return hash_equals($hmac_header, $calculated_hmac);
}

$hmac_header = $_SERVER['HTTP_X_SIGNATURE'];
$data = file_get_contents('php://input');
$verified = verify_webhook($data, $hmac_header);
error_log('Webhook verified: '.var_export($verified, true)); //check error.log to see the result
if($verified) {
  $jsonData = json_decode($data);
  $statusCode = $jsonData['data']['statusCode'];
  if($statusCode >= 300 && $statusCode < 400) {
    // Payment was successful, send out order, mark as paid, etc.
  }
}


// Return a 200 response here
// This example uses Express to receive webhooks
const crypto = require('crypto');
const timingSafeCompare = require('tsscmp');
const app = require('express')();

function verifyWebhook(hmac, rawBody) {
    // Retrieving the secret
    const webhookSecret = process.env.WEBHOOK_SECRET;

    const calculated = crypto
          .createHmac('sha1', webhookSecret)
          .update(rawBody, 'utf8')
          .digest('hex');
    return timingSafeCompare(calculated, hash);
}

function verifyWebhookRequest(req, res, buf, encoding) {
  if (buf && buf.length) {
    const rawBody = buf.toString(encoding || 'utf8');
    const hmac = req.get('X-Signature');
    req.webhook_verified = verify_webhook(hmac, rawBody);
  } else {
    req.webhook_verified = false;
  }
}

// Retrieve the raw body as a buffer and match all content types:
app.use(bodyParser.json({verify: verifyWebhookRequest}));

app.post('/webhook', (request, response) => {
  const payload = request.body;
  if(req.webhook_verified) {
    // Webhook is valid
    // Put your logic here to handle the webhook event

    if(payload.data.statusCode >= 300 && payload.data.statusCode < 400) {
      // Payment was successful, send out order, mark as paid, etc.

    }
  }
  response.send(200);
});

Responding to an event

In order for us to acknowledge that you have received the POST request, your server should respond with a 200 HTTP status. If your server responds with a 400 or higher HTTP status code we'll retry sending the POST request up to six times every ten minutes.

Webhook payload

The webhook's POST request payload contains the charge itself. Here's what a successful charge looks like.

If you'd like to learn more about the different status codes check the onChargeUpdated callback section.

{
  "id": "1234",
  "type:": "charge.success",
  "data": {
    "id": "9a4f03cb-1948-4780-81ba-b8a53a7f6468",
    "amount": "5",
    "metadata": {
      "firstName": "John",
      "lastName": "Doe",
      "email": "john@company.com"
    },
    "createdAt": "2018-08-17T18:43:11.808Z",
    "expiresAt": "2018-08-17T18:58:11.733Z",
    "description": null,
    "statusCode": 300,
    "redirectUrl": null,
    "status": "success",
    "currency": "USD",
    "organization": "pk_viqah4squsnuw6p0e1ue",
    "timeline": [
      {
        "id": "4fd18c22-98e3-4200-93a1-8fe85ba0526d",
        "time": "2018-08-17T18:43:11.808Z",
        "type": "CHARGE_CREATED",
        "status": "pending"
      },
      {
        "id": "60aa266c-b5ad-4ca9-ba5b-cd59593c0e46",
        "time": "2018-08-17T18:43:17.884Z",
        "type": "ETHEREUM_TRANSACTION_CREATED",
        "status": "processing"
      },
      {
        "id": "79690a0e-b199-4ff5-bc06-74311aea80e5",
        "time": "2018-08-17T18:43:49.051Z",
        "type": "ETHEREUM_TRANSACTION_CONFIRMED",
        "status": "processing"
      },
      {
        "id": "d3937d73-def9-4015-acaf-8f109c87a67d",
        "time": "2018-08-17T18:44:35.822Z",
        "type": "ETHEREUM_TRANSACTION_CONFIRMED",
        "status": "processing"
      },
      {
        "id": "0fbfd113-7237-4468-9d5d-79c07136e2ef",
        "time": "2018-08-17T18:44:54.033Z",
        "type": "ETHEREUM_TRANSACTION_CONFIRMED",
        "status": "success"
      }
    ],
    "payments": {
      "ethereum": {
        "transactions": [
          {
            "hash": "0xdf12ce66a4a2b54b3465c6f5394963cd759dab24142f339e4b675ad7d73dd6fa",
            "amount": "0.01743",
            "confirmations": 3
          }
        ],
        "requiredConfirmations": 3,
        "balance": "0.01743",
        "pending": "0",
        "outstanding": "0"
      }
    },
    "quotes": {
      "ethereum": {
        "id": "ccceb362-2c11-4a08-ba3c-bbb60ba46110",
        "rate": "0.003486",
        "amount": "0.01743",
        "index": "205",
        "extraId": null,
        "status": "ACTIVE",
        "address": "0x9617fa5ff408dfc0115e0ae1532fa9e75428de61",
        "currency": "ETH"
      }
    }
  }
}

results matching ""

    No results matching ""