Webhooks
The /webhooks
endpoint allows to create, list and modify webhooks. It requires authentication.
Webhook format
{
"url": "https://example.com/my-webhooks",
"secret": "<your webhook secret>",
"types": [
"charge.processing",
"charge.pending",
"charge.success",
"charge.failure",
"charge.late",
"charge.timeout",
"charge.overpaid",
"charge.underpaid",
"charge.resolved",
"charge.unsettled"
],
"organizationId": "<organization id>"
}
The types
property can contain an array of charge.<status name>
where status name
is any of the charge status code names.
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
caution
Only charges with a statusCode that is greater than, or equal to, 300
and less than 400
are considered successful so you should only mark orders as paid or send out a customer's order if a charge has code between those values.
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
- PHP
- Node.js
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 20 times every five minutes.
Webhook payload
The webhook's POST
request payload contains the charge
itself.
Here's what a successful charge
looks like.
info
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"
}
}
}
}