Skip to content

Commit 340aab6

Browse files
webhook samples
1 parent a7e4696 commit 340aab6

File tree

3 files changed

+138
-6
lines changed

3 files changed

+138
-6
lines changed

README.md

+134-5
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,13 @@ class PaymentProcessor
237237
{
238238
while (true) {
239239
echo "\nRetrieving Payments...";
240-
$this->FetchPayments();
240+
$this->fetchAndProcessPayments();
241241
echo "\nSleeping for 5 seconds...";
242242
sleep(5); // Sleep for 5 seconds before the next polling
243243
}
244244
}
245245

246-
private function FetchPayments()
246+
private function fetchAndProcessPayments()
247247
{
248248
$limit = 100; // Number of records to retrieve depending on your processing requirement & capacity
249249
$response = $this->api->getPayments($this->lastTimeStamp, $limit);
@@ -255,7 +255,7 @@ class PaymentProcessor
255255
}
256256
foreach ($response->res as $obj) {
257257
$payment = new Payment($obj);
258-
$this->ProcessPayment($payment);
258+
$this->processPayment($payment);
259259
echo "\n-----------------------------";
260260
}
261261

@@ -271,8 +271,11 @@ class PaymentProcessor
271271
}
272272
}
273273

274-
// Process Payment should be impleneted as idempotent operation for production use cases
275-
private function ProcessPayment(Payment $payment)
274+
/**
275+
* Process Payment should be impleneted as idempotent operation for production use cases
276+
* This method and logic can be shared among all payment processing consumers: 1. bulk polling, 2. webhook, 3. single payment polling.
277+
*/
278+
private function processPayment(Payment $payment)
276279
{
277280
echo "\nPayment Status: " . $payment->status;
278281
if ($payment->IsPaid()) {
@@ -296,6 +299,132 @@ $processor->Run();
296299

297300
```
298301

302+
### Webhooks - Payment processing using Webhook Callbacks
303+
304+
```php
305+
306+
<?php
307+
308+
require 'vendor/autoload.php';
309+
310+
use WeBirr\Payment;
311+
312+
// Webhook handler for processing payment updates from WeBirr.
313+
// This script should be hosted on a secure server with HTTPS enabled.
314+
315+
class Webhook
316+
{
317+
/**
318+
* Handle incoming webhook POST requests.
319+
* Validates request method (must be POST).
320+
* Checks authentication using the authKey from the query string, otherwise system will not know if the request is coming from WeBirr(authorized) or not
321+
*/
322+
public function handleRequest()
323+
{
324+
// Validate request method is POST
325+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
326+
http_response_code(405); // Method Not Allowed
327+
header('Content-Type: application/json');
328+
echo json_encode(["error" => "Method Not Allowed. POST required."]);
329+
return;
330+
}
331+
332+
// Authenticate using authKey query string parameter, otherwise system will not know if the request is from WeBirr(authorized) or not
333+
if (!$this->isAuthenticated()) {
334+
http_response_code(403);
335+
header('Content-Type: application/json');
336+
echo json_encode(["error" => "Unauthorized access. Invalid authKey."]);
337+
return;
338+
}
339+
340+
// Read the raw JSON input
341+
$rawPayload = file_get_contents('php://input');
342+
if (empty($rawPayload)) {
343+
http_response_code(400); // Bad Request
344+
header('Content-Type: application/json');
345+
echo json_encode(["error" => "Empty request body."]);
346+
return;
347+
}
348+
349+
// Decode the JSON payload into an associative array
350+
$jsonBody = json_decode($rawPayload, true);
351+
if (json_last_error() !== JSON_ERROR_NONE) {
352+
http_response_code(400);
353+
header('Content-Type: application/json');
354+
echo json_encode(["error" => "Invalid JSON format."]);
355+
return;
356+
}
357+
358+
try {
359+
$payment = new Payment($jsonBody['data']);
360+
} catch (Exception $e) {
361+
http_response_code(400);
362+
header('Content-Type: application/json');
363+
echo json_encode(["error" => "Invalid payment data: " . $e->getMessage()]);
364+
return;
365+
}
366+
367+
// Process the payment asynchronously (or enqueue it for later processing)
368+
// here instead of immediate processing, this should be handled asynchronously or enqueued to a background worker/job.
369+
$this->processPayment($payment);
370+
371+
// Return a JSON success response (can also be just empty body with 200 OK status)
372+
http_response_code(200);
373+
header('Content-Type: application/json');
374+
echo json_encode(["success" => true, "message" => "Payment received and queued for processing"]);
375+
}
376+
377+
/**
378+
* Authenticate the request using authKey from the query string.
379+
*
380+
* The authKey is compared against the environment variable `WB_WEBHOOK_AUTH_KEY`.
381+
*
382+
* @return bool True if authentication succeeds, false otherwise.
383+
*/
384+
private function isAuthenticated(): bool
385+
{
386+
// Retrieve the authKey from the query string
387+
$providedAuthKey = $_GET['authKey'] ?? '';
388+
389+
// TODO: Replace this with your own auth key (preferably from environment variable)
390+
$expectedAuthKey = "please-change-me-to-secure-key-5114831AFD5D4646901DCDAC58B92F8E";
391+
//$expectedAuthKey = getenv('wb_webhook_authkey') !== false ? getenv('wb_webhook_authkey') : "";
392+
393+
// Compare the provided key with the expected key
394+
return !empty($expectedAuthKey) && hash_equals($expectedAuthKey, $providedAuthKey);
395+
}
396+
397+
/**
398+
* Process Payment should be impleneted as idempotent operation for production use cases
399+
* Prefered approach is: Payment should be processed asynchronously or enqueued to a background worker.
400+
* This method and logic can be shared among all payment processing consumers: 1. bulk polling, 2. webhook, 3. single payment polling.
401+
* @param Payment $payment The Payment object.
402+
*/
403+
private function processPayment(Payment $payment)
404+
{
405+
echo "\nPayment Status: " . $payment->status;
406+
if ($payment->IsPaid()) {
407+
echo "\nPayment Status Text: Paid.";
408+
}
409+
if ($payment->IsReversed()) {
410+
echo "\nPayment Status Text: Reversed.";
411+
}
412+
echo "\nBank: " . $payment->bankID;
413+
echo "\nBank Reference Number: " . $payment->paymentReference;
414+
echo "\nAmount Paid: " . $payment->amount;
415+
echo "\nPayment Date: " . $payment->paymentDate;
416+
echo "\nReversal/Cancel Date: " . $payment->canceledTime;
417+
echo "\nUpdate Timestamp: " . $payment->updateTimeStamp;
418+
}
419+
420+
}
421+
422+
// Instantiate and handle the webhook request. once hosted, the url needs to be shared with WeBirr for configuration
423+
$webhook = new Webhook();
424+
$webhook->handleRequest();
425+
426+
```
427+
299428
### Gettting basic Statistics about bills created and payments received for a date range
300429

301430
```php

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "webirr/webirr",
33
"type": "library",
4-
"version": "2.0.2",
4+
"version": "2.0.3",
55
"description": "Official PHP Client Library for WeBirr Payment Gateway APIs",
66
"keywords": ["ethiopia", "fintech"],
77
"homepage": "https://github.com/webirr/webirr-api-php-client",

examples/example6-payment-status-webhook.php

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
use WeBirr\Payment;
66

7+
// Webhook handler for processing payment updates from WeBirr.
8+
// This script should be hosted on a secure server with HTTPS enabled.
9+
710
class Webhook
811
{
912
/**

0 commit comments

Comments
 (0)