الأحداث
ثلاثة من أحداث العقد التسعة تُرسل اليوم. الاشتراك في حدث لا يُرسل بعد مرفوض.
إصدارات الأطر (GET /v1/frameworks) لا تُرسل حدثًا بعد. لترى الجديد منها، اسرد GET /v1/framework-versions?framework=… دوريًا.
| الحدث | الحمولة | النطاق | يُرسل | المعنى |
|---|---|---|---|---|
| curriculum.version.published | curriculum_id, version, published_at | curriculum:read | يُرسل | نُشرت نسخة منهج، والـ Erratum أيضًا نسخةً برقم تصحيح أعلى. |
| curriculum.version.deprecated | curriculum_id, version, superseded_by? | curriculum:read | يُرسل | أُوقفت نسخة، أو استُبدلت (ومعها النسخة البديلة). |
| outcome.changed | outcome_id, curriculum_version_id, diff_class, element_revision | outcome:read | يُرسل | تغيّر نتاج: تُرُوجع عنه (DEPRECATED)، أو صحّحه Erratum (صنف الفرق ورقم المراجعة الجديد). |
| association.changed | association_id, curriculum_version_id, diff_class | association:read | ليس بعد | تغيّرت علاقة في الجراف. |
| alignment.approved | alignment_id, mapping_set_id, type | alignment:read | ليس بعد | اعتُمدت مواءمة بين منهجين. |
| mapping_set.needs_revalidation | mapping_set_id, curriculum_version_pair, reason | alignment:read | ليس بعد | مجموعة ربط تحتاج إعادة تحقق بعد نشر. |
| candidate.review_required | candidate_id, assertion_method, element_status | review:act | ليس بعد | مرشّح بانتظار المراجعة (داخلي فقط). |
| license.changed | content_license_id, scope, affected_element_ids[] | curriculum:read | ليس بعد | تغيّر ترخيص محتوى — معرّفات فقط بلا محتوى. |
| tenant.access_revoked | tenant_id, access_entitlement_id, revoked_at | admin:tenant | ليس بعد | سُحب استحقاق وصول جهة. |
لا يوجد حدث منفصل للـ Erratum. يصل نشرًا لنسخة التصحيح (curriculum.version.published)، ومعه outcome.changed لكل نتاج صحّحه.
زر «أرسل حدثًا تجريبيًا» في صفحة المطورين يرسل webhook.test بمعرّف الاشتراك، لهذا الاشتراك وحده.
التسليم
- التسليم مرة واحدة على الأقل، فقد يصل الحدث نفسه مرتين. الترويسة Currixa-Event-Id مفتاح منع التكرار، وهي ثابتة في كل إعادة محاولة وكل إعادة إرسال. عالج كل معرّف مرة واحدة.
- كل تسليم موقّع بـ HMAC-SHA256 على الوقت والجسم الخام، بسرّ اشتراكك، في الترويسة Currixa-Signature. ارفض توقيعًا عمره أكثر من خمس دقائق.
- النجاح رد 2xx خلال 10 ثوانٍ. غير ذلك يُعاد بعد 1 و5 و15 و30 و60 دقيقة: انتهاء المهلة، وإعادة التوجيه 3xx (لا تُتبع أبدًا)، و4xx و5xx. بعد الإعادة الخامسة يصير التسليم ميتًا، وتعيد إرساله من صفحة المطورين.
- الحمولة معرّفات وأرقام نسخ فقط. اقرأ المحتوى من الـ API بنطاقاتك.
- يصلك الحدث فقط إن كانت جهتك تقرأ مورده: محتوى عام، أو محتوى جهتك، أو محتوى مرخّص لكم استحقاقه. اشتراك مفتاح التجربة يستقبل المحتوى العام وحده.
- صفحة المطورين تعرض كل تسليم في آخر 30 يومًا: حالته ووقته وكود الرد، بلا جسم الرد.
Currixa-Event-Id: معرّف الحدث، ومفتاح منع التكرار.Currixa-Event-Type: اسم الحدث.Currixa-Signature: t=<ثواني يونكس>,v1=<hex لـ HMAC-SHA256 على "<t>.<الجسم الخام>">.
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Currixa-Webhooks/1
Currixa-Event-Id: crx_whe_01912d3a-0000-7000-8000-000000000000
Currixa-Event-Type: curriculum.version.published
Currixa-Signature: t=1760400000,v1=5d41402abc4b2a76b9719d911017c592…
{"id":"crx_whe_01912d3a-0000-7000-8000-000000000000","type":"curriculum.version.published",
"created_at":"2026-10-14T08:00:00.000Z",
"data":{"curriculum_id":"crx_cur_01912d3a-0000-7000-8000-000000000001","version":"2026.1.0","published_at":"2026-10-14T08:00:00.000Z"}}المعرّفات والتوقيع أعلاه توضيحية.
التحقق من التوقيع
احسب HMAC-SHA256 للوقت ثم نقطة ثم الجسم الخام كما وصل، بسرّك، وقارنه بـ v1 بزمن ثابت. لا تُعِد ترميز جسم محلَّل: تغيّر بايت واحد يُفشل التحقق.
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the request body exactly as it arrived (a Buffer), before any JSON parsing.
// header: the Currixa-Signature header. secret: your subscription's whsec_… secret.
export function verifyCurrixa(rawBody: Buffer, header: string, secret: string, toleranceSeconds = 300): boolean {
const parts = new Map(header.split(',').map((p) => [p.slice(0, p.indexOf('=')), p.slice(p.indexOf('=') + 1)]));
const t = Number(parts.get('t'));
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
const given = Buffer.from(parts.get('v1') ?? '', 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}import hashlib
import hmac
import time
def verify_currixa(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
"""raw_body: the request body exactly as it arrived; header: Currixa-Signature."""
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))# body.json holds the raw body; SIG the Currixa-Signature header; WEBHOOK_SECRET your whsec_… secret.
t=${SIG#t=}; t=${t%%,*}
v1=${SIG##*v1=}
calc=$( { printf '%s.' "$t"; cat body.json; } | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $NF}')
[ "$calc" = "$v1" ] && echo valid || echo invalidأمن العناوين
عنوان الاشتراك HTTPS فقط، بلا اسم مستخدم أو كلمة مرور أو جزء بعد #. يُحلّ الاسم، ويجب أن يكون كل عنوان محلول عامًا. تُرفض العناوين الخاصة والداخلية والـ loopback والـ link-local وعناوين البيانات الوصفية السحابية وصيغها في IPv6، عند الاشتراك وقبل كل تسليم.
تتصل Currixa بالعنوان نفسه الذي فحصته للتو، فلا يمكن تحويل الاسم بين الفحص والاتصال. لا تُتبع إعادة التوجيه، ويُقرأ الرد حتى 64 كيلوبايت ولا يُحفظ.
الاشتراك
اشترك من خادمك بمفتاح API يحمل webhook:manage. الرد يحمل سرّ التوقيع مرة واحدة: احفظه على خادمك، فلن يظهر ثانية.
export CURRIXA_API_KEY="paste-your-key-here"
curl -s https://api.currixa.net/v1/webhooks \
-H "Authorization: Bearer $CURRIXA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/currixa/webhooks","events":["curriculum.version.published","outcome.changed"]}'لا يُمنح webhook:manage افتراضيًا. مالك جهتك يمنحه لتطبيق من صفحة المطورين، ثم ينشئ له مفتاحًا. المفتاح يرى ويحذف اشتراكات تطبيقه وحده (GET وDELETE على /v1/webhooks/{id}). والمسار POST /v1/webhooks/{id}/rotate-secret ينشئ سرًّا جديدًا، ويتوقف القديم فورًا.
خطتك تحدد عدد الاشتراكات المسموح لجهتك (اشتراكات الـ webhooks في صفحة الأسعار).