Webhooks

Introduction

Webhooks are an important part of your payment integration. They allow Just Wallet notify you about events that happen on your account, such as a charge or payment transaction. A webhook URL is an endpoint on your server where you can receive notifications about such events. When an event occurs, we'll make a POST request to that endpoint, with a JSON body containing the details about the event, including the type of event and the data associated with it.

Enabling webhooks

Here's how to set up a webhook on your Just Wallet account:

  1. Log in to your dashboard and click on Settings
  2. Navigate to Webhooks to add your webhook URL
  3. Check all the boxes and save your settings

Verifying webhook signatures

When enabling webhooks, you have to set a webhook secret. Since webhook URLs are publicly accessible, the webhook secret allows you to verify that incoming requests are from Just Wallet. You can specify any value as your secret hash, but we recommend something random. You should also store it as an environment variable on your server.
You must specify a webhook secret, as we'll include it in our request to your webhook URL, in a header called Signature. In the webhook endpoint, check if the Signature header is present and that it matches the secret hash you set. If the header is missing, or the value doesn't match, you can discard the request, as it isn't from Just Wallet.

Responding to webhook requests

To acknowledge receipt of a webhook, your endpoint must return a 200 HTTP status code. Any other response codes, including 3xx codes, will be treated as a failure. We don't care about the response body or headers.

You may need to disable CSRF protection

Some web frameworks like Rails or Django, automatically check that every POST request contains a CSRF token. This is a useful security feature that protects you and your users from cross-site request forgery.

Webhook Sample Implementations

PHP/Laravel

                              
                                  // In a Laravel-like app:
                                  Route::post('webhook', function (\Illuminate\Http\Request $request) {
                                      //check for the signature
                                      $secret = 'your-secret';
                                      $signature = $request->header('signature');
                                      $sign_secret = hash_hmac('sha256', json_encode($request->all()), $secret);
                                      if (!$signature || ($signature !== $sign_secret)) {
                                          // This request isn't from Just Wallet; discard
                                          abort(401);
                                      }
                                      $payload = $request->all();
                                      // It's a good idea to log all received events.
                                      Log::info($payload);
                                      // Do something (that doesn't take too long) with the payload
                                      return response(200);
                                  });
                              
                            

Ruby

                              
                                  require 'sinatra'
                                    require 'json'
                                    require 'openssl'
                                    set :port, 3000 # Replace with your desired port

                                    post '/webhook' do
                                      # Check for the signature
                                      secret = 'your-secret'
                                      signature = request.env['HTTP_WEBHOOK_SECRET']

                                      sign_secret = OpenSSL::HMAC.hexdigest('sha256', secret, JSON.generate(request.params))

                                      if !signature || (signature != sign_secret)
                                        # This request isn't from Just Wallet; discard
                                        halt 401, 'Unauthorized'
                                      end

                                      payload = request.params

                                      # It's a good idea to log all received events.
                                      puts payload

                                      # Do something (that doesn't take too long) with the payload

                                      status 200
                                      body 'OK'
                                    end

                              
                        

Javascript

                            
                            const express = require('express');
                            const crypto = require('crypto');
                            const bodyParser = require('body-parser');

                            const app = express();
                            const port = 3000; // Replace with your desired port

                            app.use(bodyParser.json());

                            app.post('/webhook', (req, res) => {
                                // Check for the signature
                                const secret = 'your-secret';
                                const signature = req.get('signature');

                                const signSecret = crypto.createHmac('sha256', secret)
                                                       .update(JSON.stringify(req.body))
                                                       .digest('hex');

                                if (!signature || (signature !== signSecret)) {
                                    // This request isn't from Just Wallet; discard
                                    return res.status(401).send('Unauthorized');
                                }

                                const payload = req.body;

                                // It's a good idea to log all received events.
                                console.log(payload);

                                // Do something (that doesn't take too long) with the payload

                                return res.status(200).send('OK');
                            });

                            app.listen(port, () => {
                                console.log(`Server is running on port ${port}`);
                            });

                          
                        

Webhook events

Event Descriptions
payout Payout event.
collection Account funding event.
quote_creation.success Masspay Quote creation event.
quote_creation.failed Masspay Quote creation event.
quote_funding.success Masspay Quote funding event.
quote_funding.failed Masspay Quote funding event.

Example Request for Payout

                            
                              {
                                  "event.type": "payout",
                                  "data": {
                                    "id": "e32a4bb3-7553-4731-962c-d42092123c19",
                                    "recipient_id": "a90660b9-b045-4df5-a322-deb4a618a846",
                                    "bank_id": "c3da3e91-2939-4b1b-9bee-7f6fa0a8948c",
                                    "rail": "LOCAL",
                                    "currency": "USD",
                                    "amount": "20.15",
                                    "charge": "0.15",
                                    "status": "pending",
                                    "created_at": "2024-02-09T12:08:43.000000Z",
                                    "completed_at": "2024-02-09T12:08:43.000000Z"
                                  }
                                }
                            
                        

Request body

Fields Descriptions
id Unique reference number for every payout sent by client.
recipient_id Unique reference number of recipient.
currency Currency code for payout sent.
amount Amount remitted to beneficiary.
charge Processing charge.
status Payment status values (pending, initiated, processing, completed, failed, refunded, reversed).
created_at Timestamp of payout initiation.
completed_at Timestamp of payout completed.

Example Request for Collection

                          
                              {
                              "event.type": "funding",
                              "data": {
                                "id": "d52a4bb3-8553-4731-c62c-f42092123c19",
                                "currency": "USD",
                                "amount": "100.00",
                                "charge": "0.00",
                                "status": "pending",
                                "created_at": "2024-02-09T12:08:43.000000Z",
                                "updated_at": "2024-02-09T12:08:43.000000Z"
                              }
                            }
                          
                        

Request body

Fields Descriptions
id Unique reference number for transaction.
currency Currency code for transaction.
amount Amount added to wallet.
charge Processing charge.
status Payment status values (pending, success, failed, refunded, reversed).
created_at Timestamp of funding initiation.
updated_at Timestamp of funding completed.
Example Response for Quote Creation Success
                          
                              {
                                "event.type": "quote_creation.success",
                                "meta": [
                                  "a764dd44-5e09-42d4-bbad-5dea94de57f8"
                                ],
                                "data": {
                                  "id": "608c53fb-c4ec-4d33-9daf-141428c46bdd",
                                  "funded": false,
                                  "mode": "live",
                                  "quote": {
                                    "USD": {
                                      "amount": 1600,
                                      "charge": 17,
                                      "total": 1617
                                    }
                                  },
                                  "errors": null
                                }
                              }
                          
                        
Example Response for Quote Creation Failed
                          
                              {
                                "event.type": "quote_creation.failed",
                                "meta": null,
                                "data": {
                                  "errors": {
                                    "ce59229f-c200-4f2a-9ce8-21415681ef24": [
                                      "amount field is required for a recipient",
                                      "Row 1 is missing some fields, check id, amount, and currency"
                                    ]
                                  }
                                }
                              }
                          
                        
Example Response for Quote Funding Success
                          
                            {
                              "event.type": "quote_funding.success",
                              "meta": null,
                              "data": {
                                "id": "d6200d7c-d254-403e-9805-ce97363f9a1e",
                                "funded": true,
                                "mode": "live",
                                "errors": null
                              }
                            }
                          
                        
Example Response for Quote Funding Failed
                          
                            {
                              "event.type": "quote_funding.failed",
                              "meta": null,
                              "data": {
                                "id": "d6200d7c-d254-403e-9805-ce97363f9a1e",
                                "funded": false,
                                "mode": "live",
                                "errors": "Insufficient USD, Not enough funds to process 1617 USD"
                              }
                            }
                          
                        

Best practices

Don't rely solely on webhooks

Have a backup strategy in place, in case your webhook endpoint fails. For instance, if your webhook endpoint is throwing server errors, you won't know about any new customer payments because webhook requests will fail.

Respond quickly

Your webhook endpoint needs to respond within a certain time limit, or we'll consider it a failure and try again. Avoid doing long-running tasks or network calls in your webhook endpoint so you don't hit the timeout. If your framework supports it, you can have your webhook endpoint immediately return a 200 status code, and then perform the rest of its duties; otherwise, you should dispatch any long-running tasks to a job queue, and then respond.