Validating your endpoint

Before Alaaqat sends any event to your server, it has to be sure the URL really belongs to you. That is what validation does: Alaaqat sends a challenge to your URL, and you answer with a signature that only the holder of the webhook secret key can produce.

A webhook must be validated before it can be activated, and it is reset to Unvalidated every time you change its URL.

The challenge

When you press Validate on the webhook card (or call the validate endpoint), Alaaqat sends a GET request to your webhook URL with two query parameters:

GET /your-endpoint?payload=Xk3aQ&timestamp=1756600000 HTTP/1.1
Host: yourdomain.com
Parameter Description
payload A random 5 character string, different every time.
timestamp The Unix timestamp (seconds) of the challenge.

Your endpoint has 10 seconds to answer. A slower reply is treated exactly like a wrong one, and the webhook stays unvalidated.

The expected answer

Concatenate payload and timestamp in that order, with nothing in between, sign the result with HMAC SHA-256 using your webhook secret key, and return the lowercase hexadecimal digest as the whole response body:

signature = hmac_sha256(payload + timestamp, secret)

The response must:

  • have a 2xx status code,
  • contain the signature and nothing else — no JSON wrapper, no quotes, no trailing newline or whitespace.

The body is compared to the expected signature character by character, so a single extra newline fails the check.

Examples

The secret key is the one shown on the webhook card in Settings > Integrations > Webhooks. Read it from your own configuration — never hard-code it in a public repository.

PHP

$secret = getenv('ALAAQAT_WEBHOOK_SECRET');

if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    $signature = hash_hmac('sha256', $_GET['payload'].$_GET['timestamp'], $secret);

    header('Content-Type: text/plain');
    echo $signature; // no newline
    exit;
}

Laravel

Route::get('/alaaqat-webhook', function (Request $request) {
    $signature = hash_hmac(
        'sha256',
        $request->query('payload').$request->query('timestamp'),
        config('services.alaaqat.webhook_secret')
    );

    return response($signature)->header('Content-Type', 'text/plain');
});

Node.js (Express)

const crypto = require('crypto');

app.get('/alaaqat-webhook', (req, res) => {
    const signature = crypto
        .createHmac('sha256', process.env.ALAAQAT_WEBHOOK_SECRET)
        .update(req.query.payload + req.query.timestamp)
        .digest('hex');

    res.type('text/plain').send(signature);
});

Python (Flask)

import hashlib
import hmac
import os

@app.get('/alaaqat-webhook')
def validate():
    message = request.args['payload'] + request.args['timestamp']
    signature = hmac.new(
        os.environ['ALAAQAT_WEBHOOK_SECRET'].encode(),
        message.encode(),
        hashlib.sha256,
    ).hexdigest()

    return signature, 200, {'Content-Type': 'text/plain'}

One URL, two kinds of request

The same URL receives both the validation challenge and the event deliveries, so branch on the HTTP method:

  • GET → a validation challenge; answer with the signature.
  • POST → an event delivery; answer with any 2xx.

After a successful validation

The webhook becomes Validated and the Activate button becomes available. Activate it and the events you subscribed to start arriving.

If validation fails

Alaaqat answers with 403 and the message:

{
    "message": "Unable to validate the webhook. Please ensure that the secret key is correct and the webhook endpoint is properly configured."
}

Check the following:

  • The URL is publicly reachable from the internet — localhost and private addresses will not work. Use a tunnelling tool while developing.
  • The TLS certificate is valid and not self-signed.
  • No login page, IP allow-list, basic authentication, or bot protection stands in front of the endpoint.
  • The endpoint returns 200, not a redirect to another page.
  • The answer arrives within 10 seconds — anything slower is given up on.
  • The body is exactly the signature — watch for a trailing newline, a JSON wrapper such as {"signature": "..."}, or an HTML layout wrapped around it.
  • You signed payload immediately followed by timestamp, using the secret key of this webhook.
  • Both values are taken from the query string of the incoming request, not stored from an earlier attempt — they change on every challenge.