-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #64 from amcintosh/issue-63-webhooks-documentation
📝 Add webhooks documentation and example
- Loading branch information
Showing
6 changed files
with
249 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
configuration | ||
authorization | ||
api-calls/index | ||
webhooks | ||
examples | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
Webhook Callbacks | ||
================= | ||
|
||
The client supports registration and verification of FreshBooks' API Webhook Callbacks. | ||
See `FreshBooks' documentation <https://www.freshbooks.com/api/webhooks>`_ for more information. | ||
|
||
FreshBooks will send webhooks as a POST request to the registered URI with form data: | ||
|
||
.. code-block:: http | ||
name=invoice.create&object_id=1234567&account_id=6BApk&business_id=6543&identity_id=1234user_id=1 | ||
Registration | ||
------------ | ||
|
||
.. code-block:: php | ||
$clientData = array( | ||
'event' => 'invoice.create', | ||
'uri' => 'http://your_server.com/webhooks/ready' | ||
); | ||
$webhook = $freshBooksClient->callbacks()->create($accountId, data: $clientData); | ||
echo $webhook->callbackId; // 2001 | ||
echo $webhook->verified; // false | ||
Registration Verification | ||
------------------------- | ||
|
||
Registration of a webhook will cause FreshBooks to send a webhook to the specified URI with a | ||
verification code. The webhook will not be active until you send that code back to FreshBooks. | ||
|
||
.. code-block:: php | ||
$freshBooksClient->callbacks()->verify($accountId, $callbackId, $verificationCode); | ||
If needed, you can ask FreshBooks to resend the verification code. | ||
|
||
.. code-block:: php | ||
$freshBooksClient->callbacks()->resendVerification($accountId, $callbackId); | ||
Hold on to the verification code for later use (see below). | ||
|
||
|
||
Verifing Webhook Signature | ||
-------------------------- | ||
|
||
Each Webhook sent by FreshBooks includes a header, ``X-FreshBooks-Hmac-SHA256``, with a base64-encoded | ||
signature generated from a JSON string of the form data sent in the request and hashed with the token | ||
originally sent in the webhook verification process as a secret. | ||
|
||
From FreshBooks' documentation, this signature is gnerated in Python using: | ||
|
||
.. code-block:: python | ||
import base64 | ||
import hashlib | ||
import hmac | ||
import json | ||
msg = dict((k, str(v)) for k, v in message.items()) | ||
dig = hmac.new( | ||
verifier.encode("utf-8"), | ||
msg=json.dumps(msg).encode("utf-8"), | ||
digestmod=hashlib.sha256 | ||
).digest() | ||
return base64.b64encode(dig).decode() | ||
So to verify the signature in PHP: | ||
|
||
.. code-block:: php | ||
$signature = $_SERVER['HTTP_X_FRESHBOOKS_HMAC_SHA256']; | ||
$data = json_encode($_POST); | ||
// Signature from FreshBooks calculated from Python json.dumps, which | ||
// produces {"key": "val", "key2": "val"}, but PHP json_encode | ||
// produces {"key":"value","key2","val"} | ||
$data = str_replace(":", ": ", $data); | ||
$data = str_replace(",", ", ", $data); | ||
$hash = hash_hmac( | ||
'sha256', | ||
iconv(mb_detect_encoding($data), "UTF-8", $data), | ||
iconv(mb_detect_encoding($verifier), "UTF-8", $verifier), | ||
true | ||
); | ||
$calculated_signature = base64_encode($hash); | ||
$isAuthentic = $calculated_signature === $signature; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
<?php | ||
|
||
/* | ||
This is an example to show how to register for webhooks, verify them, | ||
and respond to a webhook. This requires an active server, so please | ||
look over the instructions in the examples `README.md` for setup. | ||
*/ | ||
|
||
require __DIR__ . '/../vendor/autoload.php'; | ||
|
||
use amcintosh\FreshBooks\Model\Callback; | ||
use amcintosh\FreshBooks\Model\Client; | ||
use amcintosh\FreshBooks\FreshBooksClient; | ||
use amcintosh\FreshBooks\FreshBooksClientConfig; | ||
use amcintosh\FreshBooks\Builder\PaginateBuilder; | ||
use Spryker\DecimalObject\Decimal; | ||
|
||
$fbClientId = '<your client_id>'; | ||
$accountId = '<your account_id>'; | ||
$accessToken = '<your access token>'; | ||
$uri = '<your ngrok uri>'; | ||
|
||
$conf = new FreshBooksClientConfig(accessToken: $accessToken); | ||
$freshBooksClient = new FreshBooksClient($fbClientId, $conf); | ||
|
||
function getWebhookResponse() | ||
{ | ||
$fp = fopen('./webhooks.csv', 'r'); | ||
$data = fgetcsv($fp, 1000, ","); | ||
fclose($fp); | ||
return $data; | ||
} | ||
|
||
function verifyWebhookData($verifier, $signature, $data) | ||
{ | ||
$data = json_encode($data); | ||
// Signature from FreshBooks calculated from Python json.dumps, which | ||
// produces {"key": "val", "key2": "val"}, but PHP json_encode | ||
// produces {"key":"value","key2","val"} | ||
$data = str_replace(":", ": ", $data); | ||
$data = str_replace(",", ", ", $data); | ||
$hash = hash_hmac( | ||
'sha256', | ||
iconv(mb_detect_encoding($data), "UTF-8", $data), | ||
iconv(mb_detect_encoding($verifier), "UTF-8", $verifier), | ||
true | ||
); | ||
$calculated_signature = base64_encode($hash); | ||
|
||
return $calculated_signature === $signature; | ||
} | ||
|
||
// Create a webhook callback | ||
$createData = new Callback(); | ||
$createData->event = 'client.create'; | ||
$createData->uri = $uri; | ||
|
||
echo "Creating webhook...\n"; | ||
try { | ||
$callback = $freshBooksClient->callbacks()->create($accountId, model: $createData); | ||
} catch (\amcintosh\FreshBooks\Exception\FreshBooksException $e) { | ||
echo 'Error: ' . $e->getMessage(); | ||
exit(1); | ||
} | ||
|
||
sleep(5); | ||
|
||
$webhookData = getWebhookResponse(); | ||
$webhookSignature = $webhookData[0]; | ||
$webhookParams = json_decode($webhookData[1], true); | ||
$verifier = $webhookParams['verifier']; | ||
$webhookId = $webhookParams['object_id']; | ||
|
||
echo "Recieved verification webhook for webhook_id {$webhookId} with verifier {$verifier}\n"; | ||
|
||
echo "Sending webhook verification...\n\n"; | ||
try { | ||
$freshBooksClient->callbacks()->verify($accountId, $webhookId, $verifier); | ||
} catch (\amcintosh\FreshBooks\Exception\FreshBooksException $e) { | ||
echo 'Error: ' . $e->getMessage(); | ||
exit(1); | ||
} | ||
|
||
echo "Creating client to test webhook...\n"; | ||
try { | ||
$client = $freshBooksClient->clients()->create($accountId, data: array('organization' => 'PHP Test Client')); | ||
} catch (\amcintosh\FreshBooks\Exception\FreshBooksException $e) { | ||
echo 'Error: ' . $e->getMessage(); | ||
exit(1); | ||
} | ||
echo 'Created client "' . $client->id . "\"\n"; | ||
|
||
sleep(5); | ||
|
||
$webhookData = getWebhookResponse(); | ||
$webhookSignature = $webhookData[0]; | ||
$webhookParams = json_decode($webhookData[1], true); | ||
|
||
echo "Recieved webhook {$webhookParams['name']} with id {$webhookParams['object_id']} and signature {$webhookSignature}\n"; | ||
if (verifyWebhookData($verifier, $webhookSignature, $webhookParams)) { | ||
echo "\nData validated by signature!\n"; | ||
} else { | ||
echo "\nSignature validation failed\n"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
/* | ||
This is a simple server endpoint used to demonstrate the webhook | ||
flow seen in `./webhook_flow.php`. | ||
Please look over the instructions in the examples `README.md` for setup. | ||
*/ | ||
|
||
require __DIR__ . '/../vendor/autoload.php'; | ||
|
||
if (array_key_exists('HTTP_X_FRESHBOOKS_HMAC_SHA256', $_SERVER)) { | ||
$data = array( | ||
$_SERVER['HTTP_X_FRESHBOOKS_HMAC_SHA256'], | ||
json_encode($_POST) | ||
); | ||
|
||
$fp = fopen('./webhooks.csv', 'wb'); | ||
fputcsv($fp, $data); | ||
fclose($fp); | ||
} |