تخطَّ إلى المحتوى

وثائق REST API و Webhooks

الإصدار v1 — العنوان الأساسي: https://t2.lat/api/v1. الملف الوصفي الكامل بصيغة OpenAPI 3 متاح على /api/v1/openapi.json ويمكن استيراده في Postman أو Swagger.

التوثيق والصلاحيات

أنشئ مفتاحًا من لوحة التحكم ← التكاملات وأرسله في كل طلب. يمكن إلغاء المفتاح أو تدويره في أي وقت، ولكل مفتاح صلاحيات محددة:

Authorization: Bearer t2_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
الصلاحيةتتيح
links:readقراءة الروابط
links:writeإنشاء الروابط وإدارتها
analytics:readقراءة التحليلات
forms:readقراءة النماذج
responses:readقراءة الردود

كل مفتاح يرى بيانات حسابه فقط؛ طلب مورد يخص حسابًا آخر يعيد 404 not_found.

حدود الطلبات

120 طلبًا في الدقيقة لكل مفتاح. كل رد يحمل الترويسات التالية، وعند التجاوز يعود الرمز 429 rate_limited مع الترويسة Retry-After بالثواني.

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1794477660   # توقيت Unix لتصفير النافذة

ترقيم الصفحات والفلاتر

القوائم مرتّبة من الأحدث وتُرقَّم بالمؤشر: أرسل limit (1–100، الافتراضي 25) ثم مرّر قيمة next_cursor في cursor ما دام has_more صحيحًا.

{ "data": [ ... ], "has_more": true, "next_cursor": "cmh2x1abc0001" }

فلاتر التحليلات هي نفسها فلاتر التقرير وتصدير CSV في اللوحة: from, to, tz, country, source, device, campaign, kind. بدون kind تُحسب كل الطلبات المسجلة؛ وsource=direct تعني «مباشر أو غير معروف».

الأخطاء

HTTP/1.1 409 Conflict
{ "error": { "code": "quota_exceeded", "message": "بلغت حد الخطة المجانية (10 روابط). أرشِف رابطًا لتحرير خانة ثم أعد المحاولة.", "used": 10, "limit": 10 } }
الحالةالرمزالمعنى
400invalid_request / validation_error / invalid_slugمدخلات أو فلاتر غير صالحة
401unauthenticated / invalid_keyمفتاح مفقود أو ملغى
403insufficient_scope / email_unverifiedصلاحية المفتاح لا تكفي أو البريد غير موثّق
404not_foundالمورد غير موجود أو يخص حسابًا آخر
405method_not_allowedالطريقة غير مدعومة
409quota_exceeded / slug_taken / archived / form_not_publishedتعارض مع الحصة أو الحالة
429rate_limitedتجاوز حد الطلبات
500server_errorخطأ غير متوقع — أعد المحاولة

أمثلة عملية

إنشاء رابط مختصر

curl -X POST https://t2.lat/api/v1/links \
  -H "Authorization: Bearer $T2_KEY" -H "Content-Type: application/json" \
  -d '{"destination":"https://example.com/landing?ref=autumn","name":"حملة الخريف","slug":"autumn"}'

إيقاف رابط ثم أرشفته

curl -X PATCH https://t2.lat/api/v1/links/LINK_ID -H "Authorization: Bearer $T2_KEY" \
  -H "Content-Type: application/json" -d '{"status":"paused"}'
curl -X POST https://t2.lat/api/v1/links/LINK_ID/archive -H "Authorization: Bearer $T2_KEY"

تحليلات رابط لشهر محدد، الفتحات الصالحة من السعودية فقط

curl "https://t2.lat/api/v1/links/LINK_ID/analytics?from=2026-11-01&to=2026-11-30&tz=Asia/Riyadh&kind=human&country=SA" \
  -H "Authorization: Bearer $T2_KEY"

جلب ردود نموذج صفحةً صفحة

curl "https://t2.lat/api/v1/forms/FORM_ID/responses?limit=50" -H "Authorization: Bearer $T2_KEY"
curl "https://t2.lat/api/v1/forms/FORM_ID/responses?limit=50&cursor=NEXT_CURSOR" -H "Authorization: Bearer $T2_KEY"

نقاط النهاية

get/api/v1/linksقائمة الروابط

الصلاحية: links:read

  • limit (استعلام) — عدد العناصر في الصفحة
  • cursor (استعلام) — قيمة next_cursor من الصفحة السابقة
  • status (استعلام) — القيم: active | paused | archived
  • q (استعلام) — بحث في الاسم والمسار والوجهة
الردود
  • 200 — صفحة من الروابط
    {
      "data": [
        {
          "id": "cmh2x1abc0001",
          "slug": "k7m2pq",
          "short_url": "https://t2.lat/k7m2pq",
          "name": "حملة الخريف",
          "destination": "https://example.com/landing?ref=autumn",
          "status": "active",
          "form_id": null,
          "form_required": false,
          "public_counter": false,
          "valid_opens": 42,
          "created_at": "2026-11-12T09:30:00.000Z",
          "archived_at": null
        }
      ],
      "has_more": false,
      "next_cursor": null
    }
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
post/api/v1/linksإنشاء رابط مختصر

الصلاحية: links:write. يستهلك خانة من الحصة.

مثال جسم الطلب
{
  "destination": "https://example.com/landing?ref=autumn",
  "name": "حملة الخريف",
  "slug": "autumn"
}
الردود
  • 201 — أُنشئ الرابط
    {
      "id": "cmh2x1abc0001",
      "slug": "autumn",
      "short_url": "https://t2.lat/autumn",
      "name": "حملة الخريف",
      "destination": "https://example.com/landing?ref=autumn",
      "status": "active",
      "form_id": null,
      "form_required": false,
      "public_counter": false,
      "valid_opens": 42,
      "created_at": "2026-11-12T09:30:00.000Z",
      "archived_at": null
    }
  • 400 — مدخلات غير صالحة
    {
      "error": {
        "code": "invalid_request",
        "message": "يُقبل فقط عنوان بصيغة HTTP أو HTTPS."
      }
    }
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 409 — تجاوز الحصة أو مسار مستخدم
    {
      "error": {
        "code": "quota_exceeded",
        "message": "بلغت حد الخطة المجانية (10 روابط). أرشِف رابطًا لتحرير خانة ثم أعد المحاولة."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
get/api/v1/links/{id}جلب رابط

الصلاحية: links:read

  • id (مسار) — المعرّف الداخلي
الردود
  • 200 — الرابط
    {
      "id": "cmh2x1abc0001",
      "slug": "k7m2pq",
      "short_url": "https://t2.lat/k7m2pq",
      "name": "حملة الخريف",
      "destination": "https://example.com/landing?ref=autumn",
      "status": "active",
      "form_id": null,
      "form_required": false,
      "public_counter": false,
      "valid_opens": 42,
      "created_at": "2026-11-12T09:30:00.000Z",
      "archived_at": null
    }
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 404 — غير موجود أو يخص حسابًا آخر
    {
      "error": {
        "code": "not_found",
        "message": "الرابط غير موجود."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
patch/api/v1/links/{id}تعديل رابط

الصلاحية: links:write. تعديل الوجهة لا يقطع سجل الزيارات لأنه مرتبط بالمعرّف الداخلي.

  • id (مسار) — المعرّف الداخلي
مثال جسم الطلب
{
  "destination": "https://example.com/new",
  "status": "paused"
}
الردود
  • 200 — الرابط بعد التعديل
  • 400 — مدخلات غير صالحة
    {
      "error": {
        "code": "invalid_request",
        "message": "أدخل عنوانًا صالحًا يبدأ بـ http:// أو https://"
      }
    }
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 404 — غير موجود
    {
      "error": {
        "code": "not_found",
        "message": "الرابط غير موجود."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
post/api/v1/links/{id}/archiveأرشفة رابط

الصلاحية: links:write. يتوقف الرابط العام وتُحفظ التقارير وتتحرر خانته.

  • id (مسار) — المعرّف الداخلي
الردود
  • 200 — الرابط مؤرشفًا
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 404 — غير موجود
    {
      "error": {
        "code": "not_found",
        "message": "الرابط غير موجود."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
post/api/v1/links/{id}/restoreاستعادة رابط مؤرشف

الصلاحية: links:write. تُفحص السعة قبل الاستعادة.

  • id (مسار) — المعرّف الداخلي
الردود
  • 200 — الرابط فعّالًا
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 409 — لا توجد خانة متاحة
    {
      "error": {
        "code": "quota_exceeded",
        "message": "بلغت حد الخطة المجانية (10 روابط). أرشِف رابطًا لتحرير خانة ثم أعد المحاولة."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
get/api/v1/links/{id}/analyticsتحليلات رابط

الصلاحية: analytics:read

  • id (مسار) — المعرّف الداخلي
  • from (استعلام) — بداية الفترة YYYY-MM-DD (الافتراضي: قبل 29 يومًا)
  • to (استعلام) — نهاية الفترة YYYY-MM-DD (الافتراضي: اليوم)
  • tz (استعلام) — المنطقة الزمنية (IANA)
  • country (استعلام) — رمز البلد ISO-3166
  • source (استعلام) — نطاق الموقع المُحيل، أو direct للمباشر/غير المعروف
  • device (استعلام) — القيم: desktop | mobile | tablet | bot | other
  • campaign (استعلام) — قيمة utm_campaign
  • kind (استعلام) — نوع الزيارة؛ بدونه تُحسب كل الطلبات المسجلة — القيم: human | bot | preview
الردود
  • 200 — التقرير
  • 400 — فلتر غير صالح
    {
      "error": {
        "code": "invalid_request",
        "message": "صيغة التاريخ غير صالحة (YYYY-MM-DD)."
      }
    }
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 404 — غير موجود
    {
      "error": {
        "code": "not_found",
        "message": "الرابط غير موجود."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
get/api/v1/analyticsتحليلات الحساب كله

الصلاحية: analytics:read. أضف link_id لحصر التقرير في رابط.

  • link_id (استعلام)
  • from (استعلام) — بداية الفترة YYYY-MM-DD (الافتراضي: قبل 29 يومًا)
  • to (استعلام) — نهاية الفترة YYYY-MM-DD (الافتراضي: اليوم)
  • tz (استعلام) — المنطقة الزمنية (IANA)
  • country (استعلام) — رمز البلد ISO-3166
  • source (استعلام) — نطاق الموقع المُحيل، أو direct للمباشر/غير المعروف
  • device (استعلام) — القيم: desktop | mobile | tablet | bot | other
  • campaign (استعلام) — قيمة utm_campaign
  • kind (استعلام) — نوع الزيارة؛ بدونه تُحسب كل الطلبات المسجلة — القيم: human | bot | preview
الردود
  • 200 — التقرير
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
get/api/v1/formsقائمة النماذج

الصلاحية: forms:read

  • limit (استعلام) — عدد العناصر في الصفحة
  • cursor (استعلام) — قيمة next_cursor من الصفحة السابقة
الردود
  • 200 — صفحة من النماذج
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
get/api/v1/forms/{id}جلب نموذج وحقول نسخته المنشورة

الصلاحية: forms:read

  • id (مسار) — المعرّف الداخلي
الردود
  • 200 — النموذج
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 404 — غير موجود
    {
      "error": {
        "code": "not_found",
        "message": "النموذج غير موجود."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
get/api/v1/forms/{id}/responsesردود نموذج

الصلاحية: responses:read. الأحدث أولًا.

  • id (مسار) — المعرّف الداخلي
  • limit (استعلام) — عدد العناصر في الصفحة
  • cursor (استعلام) — قيمة next_cursor من الصفحة السابقة
  • from (استعلام)
  • to (استعلام)
الردود
  • 200 — صفحة من الردود
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 404 — غير موجود
    {
      "error": {
        "code": "not_found",
        "message": "النموذج غير موجود."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }
get/api/v1/responses/{id}جلب رد

الصلاحية: responses:read

  • id (مسار) — المعرّف الداخلي
الردود
  • 200 — الرد
  • 401 — مفتاح مفقود أو غير صالح
    {
      "error": {
        "code": "invalid_key",
        "message": "مفتاح API غير صالح أو ملغى."
      }
    }
  • 403 — صلاحية المفتاح لا تكفي
    {
      "error": {
        "code": "insufficient_scope",
        "message": "هذا المفتاح لا يملك الصلاحية links:write."
      }
    }
  • 404 — غير موجود
    {
      "error": {
        "code": "not_found",
        "message": "الرد غير موجود."
      }
    }
  • 429 — تجاوز حد الطلبات
    {
      "error": {
        "code": "rate_limited",
        "message": "طلبات كثيرة. حاول بعد 12 ثانية."
      }
    }

Webhooks الموقّعة

أضف Webhook من التكاملات ← Webhooks، واختر الأحداث والحقول المرسلة. نرسل طلب POST بجسم JSON إلى عنوانك (HTTPS فقط، ولا تُتبع التحويلات، وتُمنع عناوين الشبكات الداخلية).

الحدثمتى يُرسل
link.createdإنشاء رابط
link.updatedتعديل رابط
link.archivedأرشفة رابط
form.publishedنشر نموذج
response.createdاستلام رد
POST /your/endpoint
Content-Type: application/json
X-T2-Event: response.created
X-T2-Event-Id: evt_3kPq9...           # معرّف ثابت للحدث عبر كل المحاولات
X-T2-Timestamp: 1794477600
X-T2-Signature: v1=5f2b...e91           # HMAC-SHA256

{
  "id": "evt_3kPq9...",
  "type": "response.created",
  "created_at": "2026-11-12T10:01:00.000Z",
  "data": { "id": "cmh30resp0009", "form_id": "...", "respondent_email": "sara@example.com", "answers": [ { "field_id": "f_ab12cd34", "label": "الاسم", "type": "name", "value": "سارة" } ] }
}

التحقق من التوقيع

التوقيع هو HMAC-SHA256(secret, timestamp + "." + rawBody) بترميز hex، ويُرسل بالصيغة v1=<hex>. احسبه على الجسم الخام قبل أي تحليل JSON، وقارنه مقارنة ثابتة الزمن، وارفض الطلبات التي يزيد عمر طابعها الزمني على 5 دقائق.

Node.js

import crypto from "node:crypto";

function verify(req, rawBody, secret) {
  const ts = req.headers["x-t2-timestamp"];
  const sig = req.headers["x-t2-signature"] || "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;        // حماية من إعادة الإرسال
  const expected = "v1=" + crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
  return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Python

import hmac, hashlib, time

def verify(headers, raw_body: bytes, secret: str) -> bool:
    ts = headers.get("X-T2-Timestamp", "")
    if abs(time.time() - int(ts or 0)) > 300:
        return False
    expected = "v1=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(headers.get("X-T2-Signature", ""), expected)