Skip to main content

Webhooks

Webhooks are automated messages sent from one app to another when something happens. Pomelo Connect sends webhooks to notify your application whenever a transaction changes state.

Instead of polling the API every few seconds to check whether a transaction state changed, webhooks deliver near-instant notifications. When a transaction's state changes, Pomelo sends an HTTP POST to the webhook URL you provided when creating the transaction (or set as a global webhook in your dashboard).

Webhook advantages over long-polling:

  • Efficiency — webhooks are only sent when there's new data, so no wasted API requests.
  • Real-time updates — near-instantaneous notifications, ensuring you have current transaction data.
  • Less complex — no need to maintain polling loops in your code.

Webhooks are particularly useful for events that occur infrequently or unpredictably (e.g. completion of a payment transaction). Without webhooks, your app might constantly re-check transaction state. With webhooks, Pomelo Connect notifies your app as soon as state changes.

Webhook configurations

Three ways to set up a webhook:

MethodDescription
Global WebhookSet a global webhook in the merchant dashboard under Connect → Webhooks. Receives updates about every transaction.
Single-Transaction WebhookSpecify a webhook URL in a single transaction's API request. Receives updates for only that transaction.
Webhook AppsUseful for integrations like Zapier or IFTTT, or to segment webhook events. Created programmatically via the API.

If you set up multiple of these (e.g. a global webhook + per-transaction webhook + a webhook app), events post to ALL of them — they're additive, not exclusive.

Identifying an incoming webhook:

PropertyValue
HTTP MethodPOST
X-Originator headerPomelo-Webhooks

Example webhook body

{
"created": "2023-06-09T08:46:45.899Z",
"updated": "2023-06-09T08:46:45.899Z",
"deleted": false,
"eventType": "NOTIFY_TRANSACTION_CHANGE",
"transactionId": "6482d462ce6c690008e85e42",
"state": "CONFIRMED",
"signature": "wzczRUp5i/0DqS5LzGVQUw==",
"amount": 1400,
"amountFractional": 14,
"amountFormatted": "GBP 14.00",
"currency": "GBP",
"provider": "debit_credit_card",
"qrCode": {
"url": "https://qr.example.com/transactions/6482d462ce6c690008e85e42.png"
},
"originalSignature": "wzczRUp5i/0DqS5LzGVQUw==",
"externalSource": "Pomelo Connect Gateway",
"id": "6482e6f5bcf6fa0008683c2f"
}

Setting a per-transaction webhook

NodeJS:

const axios = require('axios');

async function createPayment() {
const request = {
amount: 1000,
currency: 'GBP',
webhook: 'https://yourwebsite.com/webhook-endpoint',
// ... additional parameters
};
const response = await axios.post(
'https://api.pomelopay.com/public/v2/transactions',
request,
);
// handle response...
}

PHP:

<?php

require 'vendor/autoload.php';
use GuzzleHttp\Client;

function createPayment() {
$request = [
'amount' => 1000,
'currency' => 'GBP',
'webhook' => 'https://yourwebsite.com/webhook-endpoint',
];
$client = new Client();
$response = $client->post(
'https://api.pomelopay.com/public/v2/transactions',
['json' => $request],
);
// handle response...
}

Python:

import requests

def create_payment():
request = {
'amount': 1000,
'currency': 'GBP',
'webhook': 'https://yourwebsite.com/webhook-endpoint',
}
response = requests.post(
'https://api.pomelopay.com/public/v2/transactions',
json=request,
)
# handle response...

Processing incoming webhooks

When you receive a webhook, always re-query the API to get the authoritative final state before doing anything irreversible (shipping orders, marking invoices paid, granting access). Webhooks can be replayed or, without signature verification, spoofed.

NodeJS / Express:

const axios = require('axios');
const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhook-endpoint', async (req, res) => {
const webhookData = req.body;

// (Optional) Verify webhook signature — see below.

const transactionId = webhookData.transactionId;
const response = await axios.get(
`https://api.pomelopay.com/public/v2/transactions/${transactionId}`,
);
// ... process the authoritative response

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

app.listen(3000);

Python / Flask:

from flask import Flask, request
import requests

app = Flask(__name__)

@app.route('/webhook-endpoint', methods=['POST'])
def webhook_endpoint():
webhook_data = request.json

# (Optional) Verify webhook signature — see below.

transaction_id = webhook_data['transactionId']
response = requests.get(
f'https://api.pomelopay.com/public/v2/transactions/{transaction_id}',
)
# ... process the authoritative response

return 'OK', 200

if __name__ == '__main__':
app.run(port=3000)

PHP / Slim:

<?php

require 'vendor/autoload.php';
use GuzzleHttp\Client;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

$app = AppFactory::create();
$app->addBodyParsingMiddleware();
$client = new Client();

$app->post('/webhook-endpoint', function (Request $request, $response) use ($client) {
$webhookData = $request->getParsedBody();

// (Optional) Verify webhook signature — see below.

$transactionId = $webhookData['transactionId'];
$apiResponse = $client->get(
"https://api.pomelopay.com/public/v2/transactions/{$transactionId}",
);
// ... process the authoritative response

$response->getBody()->write('OK');
return $response;
});

$app->run();

Webhook signature verification

Verifying the signature lets you confirm a webhook actually came from Pomelo and wasn't sent by an attacker who guessed your endpoint URL.

The signature is constructed from a nonce, timestamp, and your private API key, hashed with SHA-256.

Verification algorithm:

  1. Extract these headers from the incoming request:
    • X-Signature-Nonce — unique per-request identifier
    • X-Signature-Timestamp — request timestamp
    • X-Signature — the signature to verify
  2. Concatenate nonce + timestamp + apiKey into a signString.
  3. Compute the SHA-256 hex digest of signString.
  4. Compare to the received X-Signature. Match → authentic.

NodeJS:

const crypto = require('crypto');

function verifyWebhookSignature(headers, apiKey) {
const {
'X-Signature-Nonce': nonce,
'X-Signature-Timestamp': timestamp,
'X-Signature': signature,
} = headers;
const signString = `${nonce}${timestamp}${apiKey}`;
const generated = crypto.createHash('sha256').update(signString).digest('hex');
return signature === generated;
}

PHP:

<?php

function verifyWebhookSignature($headers, $apiKey) {
$nonce = $headers['X-Signature-Nonce'];
$timestamp = $headers['X-Signature-Timestamp'];
$signature = $headers['X-Signature'];

$signString = $nonce . $timestamp . $apiKey;
$generated = hash('sha256', $signString);

return $signature === $generated;
}

Python:

import hashlib

def verify_webhook_signature(headers, api_key):
nonce = headers.get('X-Signature-Nonce')
timestamp = headers.get('X-Signature-Timestamp')
received = headers.get('X-Signature')

sign_string = f"{nonce}{timestamp}{api_key}"
generated = hashlib.sha256(sign_string.encode()).hexdigest()

return received == generated

Deprecated transaction signature verification

The older originalSignature field can be verified using your private API key with an MD5 hash. This method is deprecated as of 2023-09-01 — use the SHA-256 header-based signature above for new integrations, and always re-query the API for the source-of-truth state.

const crypto = require('crypto');
const querystring = require('querystring');

function verifyLegacySignature(webhookData, apiKey) {
const { amount, currency, originalSignature } = webhookData;
const calculated = crypto
.createHash('md5')
.update(querystring.stringify({ amount, currency, apiKey }))
.digest('base64');
return calculated === originalSignature;
}