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.
| 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. |
A webhook goes through three steps, and it delivers nothing until all three are done.
{info} Editing the URL of an existing webhook resets it to Unvalidated, and deliveries stop until you validate it again.
https://) and select the events you want to receive.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.
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).
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. |
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.
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-Deliveryis identical across every attempt of the same delivery, so store it and ignore a delivery id you have already processed.
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.
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:
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.
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.