Webhooks

تتصل Currixa بخادمك حين تُنشر نسخة منهج أو تُوقف، أو يتغيّر نتاج. التسليم موقّع ويُعاد عند الفشل، ولما تقرؤه جهتك فقط.

متاح

الأحداث

ثلاثة من أحداث العقد التسعة تُرسل اليوم. الاشتراك في حدث لا يُرسل بعد مرفوض.

إصدارات الأطر (GET /v1/frameworks) لا تُرسل حدثًا بعد. لترى الجديد منها، اسرد GET /v1/framework-versions?framework=… دوريًا.

أحداث العقد التسعة، وما يُرسل منها اليوم
الحدثالحمولةالنطاقيُرسلالمعنى
curriculum.version.publishedcurriculum_id, version, published_atcurriculum:readيُرسلنُشرت نسخة منهج، والـ Erratum أيضًا نسخةً برقم تصحيح أعلى.
curriculum.version.deprecatedcurriculum_id, version, superseded_by?curriculum:readيُرسلأُوقفت نسخة، أو استُبدلت (ومعها النسخة البديلة).
outcome.changedoutcome_id, curriculum_version_id, diff_class, element_revisionoutcome:readيُرسلتغيّر نتاج: تُرُوجع عنه (DEPRECATED)، أو صحّحه Erratum (صنف الفرق ورقم المراجعة الجديد).
association.changedassociation_id, curriculum_version_id, diff_classassociation:readليس بعدتغيّرت علاقة في الجراف.
alignment.approvedalignment_id, mapping_set_id, typealignment:readليس بعداعتُمدت مواءمة بين منهجين.
mapping_set.needs_revalidationmapping_set_id, curriculum_version_pair, reasonalignment:readليس بعدمجموعة ربط تحتاج إعادة تحقق بعد نشر.
candidate.review_requiredcandidate_id, assertion_method, element_statusreview:actليس بعدمرشّح بانتظار المراجعة (داخلي فقط).
license.changedcontent_license_id, scope, affected_element_ids[]curriculum:readليس بعدتغيّر ترخيص محتوى — معرّفات فقط بلا محتوى.
tenant.access_revokedtenant_id, access_entitlement_id, revoked_atadmin: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 بزمن ثابت. لا تُعِد ترميز جسم محلَّل: تغيّر بايت واحد يُفشل التحقق.

TypeScript (Node.js)
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);
}
Python
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", ""))
curl + openssl
# 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. الرد يحمل سرّ التوقيع مرة واحدة: احفظه على خادمك، فلن يظهر ثانية.

POST /v1/webhooks
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 في صفحة الأسعار).