Webhooks

A webhook lets Alaaqat push data to your own server the moment something happens in your account — a contact is created, a deal is updated, a note is deleted — so you never have to poll the API for changes.

Every webhook belongs to a single account, points at one HTTPS URL of yours, and is subscribed to the list of events you choose.

Requirements

Requirement Detail
Plan Webhooks are available on the Business plan, or on a custom plan that includes them. Free and Starter accounts cannot create them.
Number allowed Up to 5 webhooks per account on the Business plan.
URL A publicly reachable HTTPS URL. Plain http:// addresses are rejected.
Permissions The team member managing webhooks needs the webhook-index, webhook-create, webhook-update and webhook-delete permissions.

Lifecycle

A webhook goes through three steps, and it delivers nothing until all three are done.

  1. Create it — give Alaaqat your URL and pick the events you want. Alaaqat generates a 32 character secret key for the webhook. The new webhook starts out Unvalidated and Inactive.
  2. Validate it — Alaaqat challenges your URL and you answer with a signature built from that secret key. This proves the endpoint is really yours. See Validating your endpoint.
  3. Activate it — only a validated webhook can be activated. From that moment on, the events you subscribed to start arriving.

{info} Editing the URL of an existing webhook resets it to Unvalidated, and deliveries stop until you validate it again.

Creating a webhook from the dashboard

  1. Go to Settings > Integrations > Webhooks.
  2. Click New Webhook.
  3. Enter your URL (must start with https://) and select the events you want to receive.
  4. Click Create.

The new webhook appears as a card showing its URL, its secret key (with a copy button), its validation and activation status, and the events it is subscribed to. From the same card you can Validate, Activate / Deactivate, edit, or delete it.

You can do all of this over the API instead — see Managing webhooks.

{danger.fa-close} Treat the secret key like a password. Anyone holding it can pass the validation challenge on your behalf.

What a delivery looks like

When a subscribed event happens, Alaaqat sends a POST request with a JSON body to your URL:

POST /your-endpoint HTTP/1.1
Host: yourdomain.com
Content-Type: application/json
X-Alaaqat-Event: contact-created
X-Alaaqat-Delivery: 6f9d1c2a-1f36-4a1c-9a4e-6a3f0c2b7d11
X-Alaaqat-Timestamp: 1756600000
X-Alaaqat-Signature: sha256=6b8f0d0e2f1a7c4d9b3e5a1c8f2d4e6a0b9c7d5e3f1a2b4c6d8e0f2a4b6c8d0e

{
    "time": 1756600000.123456,
    "contact": {
        "properties": {
            "account_id": 1,
            "firstname": "test",
            "lastname": "test",
            "email": "test@alaaqat.com",
            "updated_at": "2024-05-27T22:34:23.357000Z",
            "created_at": "2024-05-27T22:34:23.357000Z",
            "_id": "66550a6f014742f5f101b824",
            "fullname": "test test",
            "image": "https://ui-avatars.com/api/?rounded=true&bold=true&name=test test"
        },
        "channels": []
    },
    "event": "contact-created"
}

The body always has three keys:

Key Description
time Unix timestamp (with microseconds) of the moment the event was queued.
event The event name, for example contact-created.
object The affected record. The key is named after the object — contact, deal_note, ticket_property … — and holds the same shape the REST API returns for that object.

The object key follows the event name:

Event Object key
contact-created contact
contact-property-updated contact_property
contact-property-group-deleted contact_property_group
contact-note-created contact_note

The same rule applies to every other object (deal, ticket, company, invoice, product).

Request headers

Every delivery carries four headers of ours on top of Content-Type: application/json:

Header Description
X-Alaaqat-Event The event name, the same value as the event key in the body.
X-Alaaqat-Delivery A UUID identifying this delivery. It stays the same across retries — use it as an idempotency key.
X-Alaaqat-Timestamp Unix timestamp of this attempt. It is regenerated on every retry, so it is always recent.
X-Alaaqat-Signature sha256= followed by the hex HMAC of the request, see Verifying a delivery.

Responding to a delivery

Reply with any 2xx status code as fast as you can; the response body is ignored. The request times out after 30 seconds.

Acknowledge the request immediately and do the real work in a background queue on your side — a slow endpoint is retried as if it had failed.

If several webhooks in the account are subscribed to the same event, each one receives its own POST. Each webhook is delivered to independently, so one failing endpoint never affects the others.

Retries

Anything other than a 2xx — an error status, a timeout, a connection that cannot be made — is retried. A delivery gets 6 attempts in total, spread over roughly a day:

Attempt Sent
1 immediately
2 5 minutes later
3 30 minutes after that
4 1 hour after that
5 3 hours after that
6 1 day after that

So a deploy, a restart, or a few hours of downtime on your side costs you nothing: the events arrive once you are back.

{warning} Retries mean the same event can reach you more than once — for example when your endpoint did the work but the response was lost. X-Alaaqat-Delivery is identical across every attempt of the same delivery, so store it and ignore a delivery id you have already processed.

When a webhook is switched off

If 3 deliveries in a row use up all six attempts — roughly three days of a dead endpoint — Alaaqat sets the webhook to inactive and stops sending to it. Every active member of the account holding the webhook-update permission is told which URL was switched off, by email and inside the dashboard.

That alert is the Webhook Deactivated notification, so each person chooses the channels it arrives on — stored, pop-up, browser, email — or silences it entirely, under Settings > Preferences > Notifications. Every channel is on by default. Keeping the email one on is worth it: a dead endpoint usually means nobody is watching the dashboard either.

The webhook stays validated, so getting it back is a single activate call (or the Activate button on the webhook card) once your endpoint is healthy. Activating it clears the failure count, and so does pointing it at a new URL, so it starts from a clean slate rather than one bad delivery away from being switched off again. Any successful delivery resets the counter too, which is why occasional failures never add up to a deactivation.

Verifying a delivery

Every delivery is signed with the webhook's secret key, so you can prove a request really came from Alaaqat and was not modified on the way.

The signature is an HMAC-SHA256 over the string "{timestamp}:{raw body}", keyed with the secret:

X-Alaaqat-Signature: sha256=<hex digest of hash_hmac('sha256', timestamp + ':' + rawBody, secret)>

Two rules matter:

  1. Hash the raw request body, exactly as received. Parsing the JSON and re-encoding it changes the bytes and the signature will not match.
  2. Compare the digests with a constant-time comparison (hash_equals, crypto.timingSafeEqual, hmac.compare_digest), never with ==.

Reject the request with 401 when the signature does not match. Also reject a delivery whose X-Alaaqat-Timestamp is more than a few minutes old — the timestamp is regenerated on every attempt, so even the last retry of a day-old delivery carries a fresh one.

PHP

$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_ALAAQAT_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_ALAAQAT_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . ':' . $body, $secret);

if (!hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

Node.js (Express, with the raw body kept)

const crypto = require('crypto');

app.post('/alaaqat-webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const timestamp = req.get('X-Alaaqat-Timestamp') || '';
    const signature = req.get('X-Alaaqat-Signature') || '';

    const expected = 'sha256=' + crypto
        .createHmac('sha256', secret)
        .update(`${timestamp}:${req.body.toString('utf8')}`)
        .digest('hex');

    const ok = expected.length === signature.length
        && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
        && Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;

    if (!ok) {
        return res.sendStatus(401);
    }

    res.sendStatus(200);
});

Python (Flask)

import hashlib, hmac, time
from flask import request, abort

@app.post('/alaaqat-webhook')
def alaaqat_webhook():
    body = request.get_data()
    timestamp = request.headers.get('X-Alaaqat-Timestamp', '')
    signature = request.headers.get('X-Alaaqat-Signature', '')

    expected = 'sha256=' + hmac.new(
        secret.encode(),
        f'{timestamp}:'.encode() + body,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, signature) or abs(time.time() - int(timestamp or 0)) > 300:
        abort(401)

    return '', 200

{info} The secret is the same key used for the validation handshake, and you can read it back at any time from the webhook card or the list endpoint.

Security

  • Only HTTPS URLs are accepted, so payloads are always encrypted in transit.
  • Every delivery is signed with the webhook's secret key — see Verifying a delivery. Verify the signature before acting on a payload; it is the only thing that tells a genuine delivery from anything else that discovers your URL.
  • Treat the secret key like a password, and deactivate or delete a webhook whose URL you no longer control.

Available events

Alaaqat emits the same twelve events for each of the six objects: contact, company, deal, ticket, invoice, product.

Take the object name, add one of these suffixes, and you have the event name — deal-created, invoice-note-updated, product-property-group-deleted, and so on.

Suffix Fires when
-created The record is created
-updated The record is updated
-deleted The record is deleted
-property-created A custom property is created
-property-updated A custom property is updated
-property-deleted A custom property is deleted
-property-group-created A property group is created
-property-group-updated A property group is updated
-property-group-deleted A property group is deleted
-note-created A note is created
-note-updated A note is updated
-note-deleted A note is deleted

For example, the full list for contacts is:

contact-created
contact-updated
contact-deleted
contact-property-created
contact-property-updated
contact-property-deleted
contact-property-group-created
contact-property-group-updated
contact-property-group-deleted
contact-note-created
contact-note-updated
contact-note-deleted

{info} Events are emitted no matter where the change came from — the dashboard, an import, or the API.