وثائق 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 } }| الحالة | الرمز | المعنى |
|---|---|---|
| 400 | invalid_request / validation_error / invalid_slug | مدخلات أو فلاتر غير صالحة |
| 401 | unauthenticated / invalid_key | مفتاح مفقود أو ملغى |
| 403 | insufficient_scope / email_unverified | صلاحية المفتاح لا تكفي أو البريد غير موثّق |
| 404 | not_found | المورد غير موجود أو يخص حسابًا آخر |
| 405 | method_not_allowed | الطريقة غير مدعومة |
| 409 | quota_exceeded / slug_taken / archived / form_not_published | تعارض مع الحصة أو الحالة |
| 429 | rate_limited | تجاوز حد الطلبات |
| 500 | server_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"نقاط النهاية
الصلاحية: 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 ثانية." } }
الصلاحية: 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 ثانية." } }
الصلاحية: 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 ثانية." } }
الصلاحية: 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 ثانية." } }
الصلاحية: 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 ثانية." } }
الصلاحية: 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 ثانية." } }
الصلاحية: 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 ثانية." } }
الصلاحية: 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 ثانية." } }
الصلاحية: 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 ثانية." } }
الصلاحية: 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 ثانية." } }
الصلاحية: 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 ثانية." } }
الصلاحية: 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": "سارة" } ] }
}- النجاح: أي رد 2xx خلال 10 ثوانٍ.
- إعادة الإرسال: عند انقطاع الاتصال أو الردود 408 / 429 / 5xx نعيد المحاولة تلقائيًا بعد 30 ثانية ثم دقيقتين ثم 10 دقائق ثم ساعة ثم 6 ساعات (6 محاولات). الردود 4xx الأخرى تُعدّ فشلًا نهائيًا، ويمكن إعادة الإرسال يدويًا من سجل المحاولات.
- عدم التكرار: قد يصلك الحدث نفسه أكثر من مرة؛ استخدم X-T2-Event-Id (وهو نفسه الحقل id) لتجاهل المكرر.
- الحقول المرسلة: إن حددت حقولًا فستحمل data تلك الحقول فقط مع id دائمًا.
التحقق من التوقيع
التوقيع هو 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)