/v1/account/webhook-endpoints
account/webhook-endpoints
Webhooks: إلى أين يرسل Monitoring الـ Signals الجديدة وكيف تتحقق منها.
نقطة نهاية webhook هي عنوان HTTPS لخادمك الذي نرسل إليه الإشارات الجديدة لمراقباتك. كل رسالة موقّعة، لكي تتحقق من أنها منّا.
التكاليف والحدود
| ماذا | الأرصدة |
|---|---|
| إدارة نقاط نهاية webhook وعمليات التسليم | مجانًا |
الحد: 30 طلب في الدقيقة. الإنشاء والتعديل والحذف: 10 في الدقيقة. الفحص والاختبار وسرّ جديد: 6 في الدقيقة.
يمكن أن يكون لكل حساب حتى 10 نقطة نهاية webhook؛ والمؤرشفة لا تُحتسب. ويمكن لكل نقطة نهاية webhook أن تحمل حتى 10 Header مخصصًا.
رمز المسؤول أو مفتاح API
تحتاج إلى رمز المسؤول (admin token) لإنشاء سرّ توقيع ولتفعيل نقاط نهاية webhook أو تعطيلها أو أرشفتها. تنشئه تحت الحساب › مفاتيح API في القسم «للمطورين: إدارة المفاتيح عبر API». وهو صالح لمدة عام، ويحل الجديد محل القديم.
أرسل رمز المسؤول كـ Authorization: Bearer YOUR_ADMIN_TOKEN. وجميع الاستدعاءات الأخرى تعمل أيضًا بمفتاح API.
كما في account/monitors تحتاج جميع استدعاءات الكتابة إلى الـ Header Idempotency-Key.
نقاط النهاية
| الطريقة | المسار | الغرض | الوصول |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
جميع نقاط نهاية webhook، دون المؤرشفة. | مفتاح API أو رمز المسؤول |
| POST |
/v1/account/webhook-endpoints |
إنشاء نقطة نهاية webhook. | رمز المسؤول |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
التحقق. | مفتاح API أو رمز المسؤول |
| POST |
/v1/account/webhook-endpoints/{id}/test |
إرسال رسالة اختبار. | مفتاح API أو رمز المسؤول |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
سرّ توقيع جديد. | رمز المسؤول |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
تفعيل. | رمز المسؤول |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
تعطيل. | رمز المسؤول |
| DELETE |
/v1/account/webhook-endpoints/{id} |
أرشفة. | رمز المسؤول |
| GET |
/v1/account/webhook-deliveries |
آخر 50 عملية تسليم. | مفتاح API أو رمز المسؤول |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
إعادة إرسال عملية تسليم فاشلة. | مفتاح API أو رمز المسؤول |
| GET |
/v1/account/webhook-events |
آخر 50 حدث. | مفتاح API أو رمز المسؤول |
إنشاء نقطة نهاية webhook
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "YOUR_RECEIVER_TOKEN" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer YOUR_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "YOUR_RECEIVER_TOKEN" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer YOUR_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "YOUR_RECEIVER_TOKEN" } }), }); const data = await response.json();
| الاسم | النوع | الوصف | مثال |
|---|---|---|---|
| Idempotency-KeyHeader | نص (string)إلزامي | قيمة فريدة لكل عملية، ويُفضَّل UUID. انظر Idempotency-Key.الحدود: من 1 إلى 128 حرفًا |
req-20260802-01 |
| nameBody | نص (string)إلزامي | اسم لمرجعك الخاص.الحدود: من 1 إلى 120 حرفًا |
CRM-Sync |
| urlBody | نص (string) (uri)إلزامي |
عنوان خادمك: يمكن الوصول إليه علنًا، https على المنفذ 443، باسم مضيف بدلًا من عنوان IP.الحدود: من 1 إلى 2,048 حرفًا |
https://hooks.example.com/handelsregister |
| headersBody | كائن (object)اختياري | Headers مخصصة تُرسل مع كل رسالة، مثل رمز لخادمك.الحدود: 10 عنصرًا على الأكثر |
{"X-Receiver-Token":"YOUR_RECEIVER_TOKEN"} |
يجب ألا تبدأ أسماء الـ Headers بـ webhook- وألا تتجاوز الـ Headers القياسية مثل Content-Type أو Host. والقيم للكتابة فقط؛ ولاحقًا لا تُظهر الـ API إلا الأسماء.
تحتوي الاستجابة على سرّ التوقيع whsec_…. ونعرضه هذه المرة فقط. احفظه فورًا.
1{2 "endpoint": {3 "id": "wep_01m6b3j1g0cdz98hw8zdefbf52",4 "name": "CRM-Sync",5 "url": "hooks.example.com/…",6 "status": "pending_verification",7 "verified_at": null,8 "header_names": [9 "X-Receiver-Token"10 ],11 "consecutive_failures": 0,12 "created_at": "2026-11-01T08:28:01+00:00"13 },14 "signing_secret": "whsec_…",15 "meta": {16 "message": "Endpoint created. Store the signing secret now — it is shown only once.",17 "request_credit_cost": 0,18 "credits_remaining": 14519 }20}
لنقطة نهاية webhook الجديدة الحالة pending_verification ولا تتلقى الرسائل إلا بعد التحقق منها. لا يمكن تغيير الاسم والـ URL والـ Headers لاحقًا. ولعنوان جديد أنشئ نقطة نهاية webhook جديدة.
في الاستجابات يُظهر url المضيف فقط؛ ونختصر المسار إلى /….
التحقق
مع POST …/{id}/verify نرسل رسالة موقّعة من النوع endpoint.verification إلى الـ URL الخاص بك. ويحتوي data.challenge على قيمة عشوائية.
يجيب خادمك خلال 15 ثانية بحالة 2xx ويُعيد القيمة في Header الاستجابة webhook-verification. وتصبح نقطة نهاية webhook حينها active.
1{2 "id": "msg_01m6b3jsqq7ntwzkdj099ayraz",3 "type": "endpoint.verification",4 "timestamp": "2026-11-01T08:28:26.743Z",5 "schema_version": 1,6 "data": {7 "challenge": "189fb34b2af819bb13cb47d9806dbf30"8 }9}
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/verify" \ -H "x-api-key: YOUR_API_KEY" \ -H "Idempotency-Key: $(uuidgen)"
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/verify", headers={ "x-api-key": "YOUR_API_KEY", "Idempotency-Key": str(uuid.uuid4()), }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/verify", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
إذا نجح ذلك، تجيب الـ API بـ "verified": true، وإلا بـ 422 و"verified": false. ولمحاولة جديدة تحتاج إلى Idempotency-Key جديد.
تسمح كل نقطة نهاية webhook بـ 6 محاولة في الساعة.
1{2 "verified": true,3 "endpoint": {4 "id": "wep_01m6b3j1g0cdz98hw8zdefbf52",5 "name": "CRM-Sync",6 "url": "hooks.example.com/…",7 "status": "active",8 "verified_at": "2026-11-01T08:28:26+00:00",9 "header_names": [10 "X-Receiver-Token"11 ],12 "consecutive_failures": 0,13 "created_at": "2026-11-01T08:28:01+00:00"14 },15 "meta": {16 "request_credit_cost": 0,17 "credits_remaining": 14518 }19}
بعد ذلك يرسل POST …/{id}/test رسالة اختبار موقّعة من النوع endpoint.test متى شئت. وتُظهر الاستجابة نتيجة التسليم.
1{2 "delivery": {3 "id": "del_01m6b3jsyrnc3sz1mt31dq6107",4 "message_id": "msg_01m6b3jsynxzhvagm7n2nq7mpv",5 "event_id": "evt_01m6b3jsynxzhvagm7n2nq7mpt",6 "event_type": "endpoint.test",7 "endpoint_id": "wep_01m6b3j1g0cdz98hw8zdefbf52",8 "status": "succeeded",9 "attempts": 1,10 "last_status_code": 200,11 "last_error_category": null,12 "next_attempt_at": "2026-11-01T08:28:26+00:00",13 "succeeded_at": "2026-11-01T08:28:26+00:00",14 "created_at": "2026-11-01T08:28:26+00:00"15 },16 "meta": {17 "request_credit_cost": 0,18 "credits_remaining": 14519 }20}
التحقق من التوقيع
كل رسالة موقّعة وفق المعيار المفتوح Standard Webhooks. تحقّق من التوقيع قبل أن تعالج رسالة.
| الـ Header | المعنى |
|---|---|
| webhook-id |
معرّف الرسالة (msg_…). ويبقى كما هو عند إعادة المحاولة. استخدمه لاكتشاف التكرارات.
|
| webhook-timestamp | وقت محاولة التسليم هذه بثواني Unix. |
| webhook-signature |
توقيع واحد أو أكثر بالصيغة v1,<Base64>، مفصولة بمسافات.
|
كيف تتحقق من رسالة:
- خذ جزء السرّ بعد
whsec_وفك ترميزه من Base64. هذا هو المفتاح. - ابنِ النص
webhook-id.webhook-timestamp.Bodyمن الـ Headers الاثنين ونص الطلب دون تغيير. - احسب منه HMAC-SHA256 بالمفتاح ورمّز النتيجة بـ Base64.
- قارن النتيجة في زمن ثابت مع كل توقيع
v1في الـ Header. إذا طابق أحدها، فالرسالة أصلية.
استخدم نص الطلب تمامًا كما يصل، وليس JSON أُعيد تسلسله. وارفض الرسائل التي يختلف طابعها الزمني بأكثر من خمس دقائق.
import base64, hashlib, hmac, time def verify_webhook(secret: str, headers: dict, body: bytes) -> bool: msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] # ارفض الرسائل الأقدم من خمس دقائق if abs(time.time() - int(timestamp)) > 300: return False key = base64.b64decode(secret.removeprefix("whsec_")) signed = f"{msg_id}.{timestamp}.".encode() + body expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode() # أثناء تدوير السرّ يحمل الـ Header توقيعين for part in headers["webhook-signature"].split(" "): version, _, signature = part.partition(",") if version == "v1" and hmac.compare_digest(signature, expected): return True return False
import crypto from "node:crypto"; export function verifyWebhook(secret, headers, rawBody) { const msgId = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; // ارفض الرسائل الأقدم من خمس دقائق if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = crypto .createHmac("sha256", key) .update(`${msgId}.${timestamp}.${rawBody}`) .digest(); // أثناء تدوير السرّ يحمل الـ Header توقيعين return headers["webhook-signature"].split(" ").some((part) => { const [version, signature] = part.split(","); const given = Buffer.from(signature ?? "", "base64"); return version === "v1" && given.length === expected.length && crypto.timingSafeEqual(given, expected); }); }
بدلًا من كتابة كودك الخاص يمكنك استخدام مكتبات Standard Webhooks الرسمية، مثل الحزمة standardwebhooks لـ Python وJavaScript.
مع POST …/{id}/rotate-secret تحصل على سرّ جديد، مرة واحدة، في الاستجابة. ولمدة 7 يومًا تحمل الرسائل توقيعين، أحدهما بالسرّ الجديد والآخر بالقديم. وبذلك تنتقل دون توقف.
الحمولة
كل رسالة كائن JSON بالغلاف نفسه. وما يشكّل الحدث موجود في data.
| الحقل | المعنى |
|---|---|
| id |
معرّف الرسالة، مساوٍ للـ Header webhook-id.
|
| event_id |
معرّف الحدث (evt_…).
|
| type | نوع الحدث، انظر أدناه. |
| timestamp | متى اكتشفنا الحدث، بتوقيت UTC. |
| schema_version |
إصدار البنية، حاليًا 1.
|
| data | بيانات الحدث. |
| نوع الحدث | المعنى |
|---|---|
| organization.signal.detected |
وجد مراقب إشارة جديدة. لـ data.signal البنية نفسها التي للإشارة في signals، إضافةً إلى data.monitor، وتحت data.links.signal الرابط إلى الإشارة في الـ API.
|
| organization.signal.sample |
مثال من التاريخ الحقيقي للشركة، يُرسل تحت الحساب › المراقبة. بالبنية نفسها كما في الأعلى، إضافةً إلى data.sample: true.
|
| endpoint.test |
رسالة اختبار عبر /test.
|
| endpoint.verification | التحدي أثناء التحقق. |
1{2 "id": "msg_01m65pqg9x0176cteqvtxw68sa",3 "event_id": "evt_01m65pqg9sx7mwmw0edc1mfbbe",4 "type": "organization.signal.sample",5 "timestamp": "2026-10-30T06:07:37.000Z",6 "schema_version": 1,7 "data": {8 "monitor": {9 "id": "mon_01m65pn3vm11sjjp1j076cbvec",10 "type": "organization.signals"11 },12 "sample": true,13 "signal": {14 "event": {15 "id": "36c7c8fddf99bb0299d6815bcaa01013",16 "topic": "TRANSFORMATIONS",17 "topic_name": {18 "de": "Umwandlungen & M&A",19 "en": "Transformations & M&A"20 },21 "occurred_on": "2022-12-22",22 "announced_on": "2023-08-10",23 "date_basis": "SOURCE_EVENT"24 },25 "organization": {26 "type": "ORGANIZATION",27 "entity_id": "94eb910fef13f94585c48c263fa446d4",28 "current_profile": {29 "name": "Hetzner Online GmbH",30 "registration": {31 "register_type": "HRB",32 "register_number": "6089",33 "court": "Ansbach",34 "register_locality": "Ansbach",35 "registration_authority_id": "D3201",36 "register_entity_name": "Ansbach HRB 6089"37 },38 "legal_form": {39 "name": {40 "de": "Gesellschaft mit beschränkter Haftung"41 },42 "short": {43 "de": "GmbH"44 }45 },46 "seat": {47 "city": "Gunzenhausen"48 },49 "status": {50 "code": "ACTIVE"51 }52 }53 },54 "parties": {55 "counterparty": {56 "entity": {57 "type": "ORGANIZATION",58 "entity_id": "dd4d2d6854b2dbd1ffce74e669f3852e",59 "current_profile": {60 "name": "Headtrip GmbH",61 "registration": {62 "register_type": "HRB",63 "register_number": "33432",64 "court": "Nürnberg",65 "register_locality": "Nuremberg",66 "registration_authority_id": "D3310",67 "register_entity_name": "Nürnberg HRB 33432"68 },69 "legal_form": {70 "name": {71 "de": "Gesellschaft mit beschränkter Haftung"72 },73 "short": {74 "de": "GmbH"75 }76 },77 "seat": {78 "city": "Nürnberg"79 },80 "status": {81 "code": "INACTIVE"82 }83 }84 },85 "relationship": "TRANSFERRING_COMPANY"86 }87 },88 "register_entry": {89 "entry_number": "3",90 "entry_date": "2023-08-10",91 "description_text": "Verschmelzung zur Aufnahme der Headtrip GmbH",92 "context": "PRIMARY",93 "flags": []94 },95 "source": {96 "kind": "COMMERCIAL_REGISTER"97 },98 "details": {99 "schema": "TRANSFORMATION",100 "kind": "MERGER",101 "specifics": {102 "merger_target": "ABSORPTION"103 },104 "direction": "INCOMING",105 "is_cross_border": false,106 "resolution_date": "2022-12-22",107 "agreement_date": "2022-12-22"108 }109 },110 "links": {111 "signal": "https://handelsregister.ai/api/v1/signals/36c7c8fddf99bb0299d6815bcaa01013"112 }113 }114}
حجم الرسالة 20 KB على الأكثر. وإذا صارت الإشارة أكبر، يُترك details وregister_entry ويكون data.truncated بقيمة true. وتجلب حينها الإشارة كاملة عبر الرابط.
تجاهل أنواع الأحداث التي لا تعرفها وأجب مع ذلك بـ 2xx. وبذلك يبقى خادمك مستقرًا عند إضافة أنواع جديدة.
عمليات التسليم وإعادة المحاولة
تُعدّ عملية التسليم ناجحة عندما يجيب خادمك خلال 15 ثانية بحالة 2xx. ولا نتبع عمليات إعادة التوجيه.
وإلا نحاول مجددًا: حتى 10 محاولة، بفواصل متزايدة على مدى نحو ثلاثة أيام. وإذا أرسل خادمك Retry-After، فإننا نلتزم به.
إذا أجاب خادمك بـ 410، نتوقف عن التسليم ونعطّل نقطة نهاية webhook.
إذا فشلت 5 عملية تسليم متتالية نهائيًا، تُعطَّل نقطة نهاية webhook مع disabled_failures. ومع POST …/{id}/enable تعيد تشغيلها.
webhook-id. أجب بسرعة وعالج الرسالة بعد ذلك فقط.
يُظهر GET /v1/account/webhook-deliveries آخر 50 عملية تسليم، ومع ?endpoint=wep_… لنقطة نهاية webhook واحدة. ويُظهر GET /v1/account/webhook-events آخر 50 حدث.
تعيد إرسال عملية تسليم فشلت نهائيًا عبر POST /v1/account/webhook-deliveries/{id}/retry، بالـ webhook-id نفسه ونص الطلب نفسه. ويعمل ذلك لمدة 30 يومًا؛ وهي المدة التي نحتفظ فيها بالرسائل.
| الحالة | المعنى |
|---|---|
| withheld | تنتظر حتى يُحتسب الفحص. |
| pending | تنتظر الإرسال. |
| in_flight | قيد الإرسال الآن. |
| retry_wait | تنتظر المحاولة التالية. |
| succeeded | سُلّمت. |
| exhausted | فشلت جميع المحاولات. |
| blocked_endpoint | لا تستطيع نقطة نهاية webhook استقبال أي شيء حاليًا. |
| cancelled | أُلغيت، مثلًا لأن الرسالة انتهت صلاحيتها. |
التفعيل والتعطيل والأرشفة
يوقف POST …/{id}/disable التسليم مؤقتًا. وتتوقف حينها مؤقتًا المراقبات التي ليس لها نقطة نهاية webhook أخرى مع paused_configuration.
يعيد POST …/{id}/enable تشغيل نقطة نهاية webhook موثّقة. ثم استأنف المراقبات الموقوفة مؤقتًا بـ resume.
يؤرشف DELETE …/{id} نقطة نهاية webhook نهائيًا. ولإيقافها لفترة، عطّلها بدلًا من ذلك.
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ -H "Idempotency-Key: $(uuidgen)"
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable", headers={ "Authorization": "Bearer YOUR_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable", { method: "POST", headers: { "Authorization": "Bearer YOUR_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
الأخطاء
للأخطاء البنية نفسها كما في account/monitors: رمز في error، والشرح في meta.message. الأخطاء لا تكلّف شيئًا.
| الحالة | السبب | ما يمكنك فعله |
|---|---|---|
| 400 |
api_key موجود في الـ URL. |
أرسل المفتاح في الـ Header x-api-key. |
| 400 |
الـ URL غير مسموح كوجهة، أو Header غير مسموح، أو تم بلوغ الحد الأقصى للعدد، أو نقطة نهاية webhook غير نشطة. ويذكر meta.message السبب. |
صحّح القيمة أو تحقّق أولًا. |
| 400 |
لـ Idempotency-Key صيغة غير صالحة. |
صحّح الصيغة، مثلًا استخدم UUID. |
| 401 | لا يوجد مفتاح API أو رمز، أو أحدهما غير صالح. |
أرسل المفتاح في الـ Header x-api-key. |
| 403 |
ينقص Bearer token الصلاحية account:read. |
استخدم مفتاح API أو رمزًا بـ account:read. |
| 403 | لم يُؤكَّد عنوان البريد الإلكتروني لحسابك بعد، أو الحساب مقفل. | افتح الرابط من بريد التأكيد. وإذا كان الحساب مقفلًا، يمكن للدعم المساعدة. |
| 403 | يحتاج هذا الاستدعاء إلى رمز المسؤول. لا يكفي مفتاح API، ولا أي رمز آخر. |
أرسله مع Authorization: Bearer ورمز المسؤول. |
| 403 |
ينقص Bearer token الصلاحية monitoring:manage. |
استخدم مفتاح API أو رمز المسؤول. |
| 404 | لا توجد نقطة نهاية webhook أو عملية تسليم بهذا المعرّف. | تحقّق من المعرّف. |
| 409 |
استُخدم Idempotency-Key بالفعل مع بيانات مختلفة، أو هو أقدم من 24 ساعة، أو لا يزال قيد المعالجة. |
استخدم مفتاحًا جديدًا أو انتظر قليلًا. |
| 422 |
قيمة مفقودة أو غير صالحة، مثل name أو url. ويذكر errors الحقل. |
صحّح القيمة وأعد المحاولة بمفتاح جديد. |
| 422 | فشل التحقق. | تحقّق من Header التحدي ومن حالة 2xx، ثم أعد المحاولة بمفتاح جديد. |
| 428 |
الـ Header Idempotency-Key مفقود. |
أرسل الـ Header. |
| 429 | طلبات كثيرة جدًا. |
أعد المحاولة بعد Retry-After. |
| 503 | التحقق والاختبار وإعادة الإرسال غير متاحة مؤقتًا. | أعد المحاولة بعد قليل. |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}