#/delivery
Delivery Insight: حالة التسليم لكل مستلم عبر REST API والبث الحي وخطافات الويب والأداة المصغّرة
يعرض Delivery Insight للتطبيق الذي يرسل عبر nextxch (POST /api/v1/send) الحالة الحية لرسائله حتى مستوى كل مستلم: تم التسليم، قيد النقل، إعادة المحاولة مع السبب والمحاولة التالية، مرتد، مستبعد أو منتهي الصلاحية. ويتضمن واجهة API للحالة، وملخص الدفعة، والبث الحي (Server-Sent Events)، وخطافات ويب موقّعة، والتدخلات في قائمة الانتظار (الإيقاف، إعادة المحاولة، إعادة الجدولة، إعادة التوجيه)، وتصنيف الارتدادات مع قائمة استبعاد لكل تطبيق، وأداة حالة مصغّرة قابلة للتضمين. عنوان URL الأساسي https://<server>/api/v1، وقابل للقراءة آليًا بتنسيق OpenAPI 3.1 تحت /api/v1/openapi.json. جزء من واجهة API العامة ولذلك اعتبارًا من Business.
الخصوصية: يُبلغ Delivery Insight فقط عمّا يجري بين خوادم البريد. لا يوجد تتبّع للفتح ولا تتبّع للنقرات، ولا بكسلات تتبّع ولا روابط مُعاد كتابتها. تعود البيانات الوصفية إليكم وحدكم ولا تُسجَّل أبدًا؛ تُحفظ أحداث البث 72 ساعة، وعمليات الإرسال والسجل 30 يومًا. يرى مفتاح المستخدم رسائل حسابه فقط، ويرى مفتاح المستأجر ما أرسله بنفسه فقط؛ وتُجاب المعرّفات الأجنبية بالرمز 404 مثل المعرّفات غير المعروفة.
1. البدء السريع: إنشاء مفتاح، الإرسال، قراءة الحالة
الإدارة > API والتكاملات > مفاتيح التكامل: النطاقان mail.send وdelivery.read (إضافةً إلى webhooks.manage لخطافات الويب وdelivery.write للتدخلات وقوائم الاستبعاد) وعنوان مرسل خاص بالتطبيق. يُعرض المفتاح smk_... مرة واحدة فقط. أو بدلًا من ذلك رمز OAuth 2.0 مميز من POST /api/v1/oauth/token (بيانات اعتماد العميل، صالح لمدة ساعة واحدة).
# send.json
{
"from": "noreply@drinklist.example",
"to": ["anna@gmx.de", "ben@web.de"],
"subject": "Drink list November",
"text": "Hello, the list is ready.",
"batchId": "drinklist-2026-11",
"tags": ["monthly", "drinklist"],
"metadata": { "listId": "2026-11" },
"idempotencyKey": "drinklist-2026-11-run-1"
}
curl -sS https://mail.example.com/api/v1/send \
-H "Authorization: Bearer $NEXTXCH_API_KEY" -H "Content-Type: application/json" -d @send.json
# Answer: sendId per call, recipientId per recipient, state queued or scheduled
curl -sS https://mail.example.com/api/v1/sends/0193a0c4-7c1e-7b8a-9f00-3c2d1e0f4a11 -H "Authorization: Bearer $NEXTXCH_API_KEY"
curl -sS https://mail.example.com/api/v1/batches/drinklist-2026-11 -H "Authorization: Bearer $NEXTXCH_API_KEY"
2. واجهة API للحالة وملخص الدفعة
يُرجع GET /api/v1/sends/{sendId} عملية الإرسال مع جميع المستلمين (attempts وnextAttemptAtUtc وlastSmtpCode وlastSmtpResponse وtargetMx وtls وthrottleReason وprovider). يسرد GET /api/v1/sends الأحدث أولًا، وبحد أقصى حتى مدة الاحتفاظ (30 يومًا)؛ وlimit افتراضيًا 50 وبحد أقصى 200، والمتابعة باستخدام cursor = pagination.nextCursor. يجمع GET /api/v1/batches/{batchId} جميع عمليات الإرسال في دفعة لعرض التقدّم.
GET /api/v1/sends?tag=monthly&status=deferred&batchId=drinklist-2026-11&since=2026-11-10T00:00:00Z&limit=50
GET /api/v1/batches/drinklist-2026-11
{
"ok": true,
"data": {
"batchId": "drinklist-2026-11", "sends": 1, "total": 400,
"delivered": 371, "inFlight": 4, "retrying": 22, "bounced": 2, "suppressed": 1,
"expired": 0, "cancelled": 0, "completed": false, "progressPercent": 93.5,
"nextAttemptAtUtc": "2026-11-10T08:05:02Z",
"retry": { "total": 22, "byReason": [{ "reason": "rate_limit", "count": 15 }],
"byProvider": [{ "provider": "GMX / WEB.DE", "count": 15 }] }
}
}
3. السجل والبث الحي (SSE) مع Last-Event-ID
كل تغيير في الحالة هو حدث delivery.queued أو delivery.deferred أو delivery.delivered أو delivery.bounced أو delivery.suppressed أو delivery.expired أو delivery.cancelled أو delivery.redirected مع رقم تسلسلي seq. يُرجع السجل GET /api/v1/events الأقدم أولًا (حتى 30 يومًا، وlimit حتى 500). يرسل البث GET /api/v1/events/stream القيم id وevent وdata؛ وكل 5 ثوانٍ يُبقي تعليق (: ping) الوكلاء (Proxies) مفتوحين. الاستئناف باستخدام ترويسة Last-Event-ID (أو ?lastEventId=) للأحداث حتى 72 ساعة إلى الوراء؛ ويقترح الخادم retry: 3000. لا مكان لمفاتيح API في المتصفحات: استخدموا هناك أداة الحالة المصغّرة.
curl -N https://mail.example.com/api/v1/events/stream?batchId=drinklist-2026-11 \
-H "Authorization: Bearer $NEXTXCH_API_KEY" -H "Last-Event-ID: 1041"
id: 1042
event: delivery.deferred
data: {"id":"evt_3f1c0d9a8b7e6f5a4b3c2d1e","seq":1042,"type":"delivery.deferred","data":{"recipient":"anna@gmx.de","state":"deferred","attempts":1,"nextAttemptAtUtc":"2026-11-10T08:05:02Z","lastSmtpCode":451,"throttleReason":"greylisting","provider":"GMX / WEB.DE"}}
: ping
# History (next page with after=pagination.nextCursor):
GET /api/v1/events?after=1000&batchId=drinklist-2026-11&limit=500
4. تسجيل خطافات الويب (Webhooks) والتحقق من التوقيع
يستجيب POST /api/v1/webhooks (النطاق webhooks.manage، ولأحداث التسليم إضافةً إلى ذلك delivery.read) بالرمز 201 مع سر التوقيع whsec_...، وذلك في هذه الاستجابة فقط. يجب تسمية أحداث التسليم صراحةً، إذ لا يشملها *. يجمّع batchSeconds من 1 إلى 60 الأحداث في مصفوفة JSON (X-Nextxch-Event: delivery.batch، بحد أقصى 500)؛ والقيمة 0 ترسل كل حدث بمفرده. التوقيع: X-Nextxch-Signature = v1= + hex(HMAC-SHA256(secret, X-Nextxch-Timestamp + . + raw body)). استخدموا دائمًا النص الخام، وقارنوا في زمن ثابت وارفضوا الطوابع الزمنية القديمة (أقدم من 5 دقائق). استجيبوا بـ 2xx خلال 10 ثوانٍ؛ وإلا يعيد nextxch المحاولة بعد 30 ثانية ودقيقتين و10 دقائق وساعة واحدة و6 ساعات بمعرّف الحدث نفسه.
POST /api/v1/webhooks
{"url":"https://drinklist.example/hooks/nextxch",
"events":["delivery.deferred","delivery.delivered","delivery.bounced","delivery.expired"],
"batchSeconds":10}
# Header: X-Nextxch-Event, X-Nextxch-Event-Id, X-Nextxch-Delivery, X-Nextxch-Webhook-Id, X-Nextxch-Timestamp, X-Nextxch-Signature
# Log: GET /api/v1/webhooks/{id}/deliveries · Test: POST /api/v1/webhooks/{id}/test
// C#
static bool VerifyWebhook(string secret, string timestamp, string signature, byte[] rawBody)
{
if (!long.TryParse(timestamp, out long ts) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300) return false;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
string expected = "v1=" + Convert.ToHexStringLower(hmac.ComputeHash([.. Encoding.ASCII.GetBytes(ts + "."), .. rawBody]));
return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(signature.Trim()));
}
// PHP
function verify_webhook(string $secret, string $timestamp, string $signature, string $rawBody): bool
{
if (!ctype_digit($timestamp) || abs(time() - (int)$timestamp) > 300) return false;
return hash_equals('v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret), trim($signature));
}
# Python
def verify_webhook(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool:
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
expected = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature.strip())
// JavaScript (Node)
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyWebhook(secret, timestamp, signature, rawBody) {
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = Buffer.from('v1=' + createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex'));
const given = Buffer.from(String(signature).trim());
return expected.length === given.length && timingSafeEqual(expected, given);
}
5. التدخلات: الإيقاف، إعادة المحاولة، إعادة الجدولة، إعادة التوجيه
النطاق delivery.write. يؤثر فقط على المستلمين المفتوحين (scheduled وqueued وdeferred)؛ ويُتخطى الآخرون مع السبب final أو in_flight أو not_in_queue. تسري إعادة المحاولة وإعادة الجدولة لكل عنصر في قائمة الانتظار، ويتحرك معه جميع المستلمين المفتوحين للعنصر نفسه. إعادة الجدولة لمدة أقصاها 30 يومًا مقدمًا، واستدعاءات الدفعات حتى 10,000 مستلم. تُنشئ إعادة التوجيه مستلمًا جديدًا (الحدث delivery.redirected، بحد أقصى 3 عمليات إعادة توجيه في السلسلة، وتُحتسب ضمن حدود الإرسال الصادر). يُسجَّل كل تدخل في سجل التدقيق الأمني (DeliveryInsight.Cancel و.Retry و.Reschedule و.Redirect) مع الأعداد والمعرّفات، وليس مع العناوين أبدًا.
POST /api/v1/sends/{sendId}/cancel # ohne Body / no body
POST /api/v1/sends/{sendId}/retry
POST /api/v1/batches/{batchId}/reschedule {"sendAtUtc":"2026-11-11T06:00:00Z"}
POST /api/v1/sends/{sendId}/recipients/{recipientId}/redirect {"to":"buchhaltung@kunde.example"}
# Antwort / answer
{"action":"retry","applied":3,"skipped":1,
"recipients":[{"recipientId":"...","email":"anna@gmx.de","state":"queued","applied":true,"reason":null,"newRecipientId":null}]}
# Fehler / errors: 400 INVALID_TIME, INVALID_ADDRESS, SAME_ADDRESS, REDIRECT_ONE_RECIPIENT
# 409 ALREADY_DELIVERED, IN_FLIGHT, REDIRECT_LIMIT, ADDRESS_SUPPRESSED · 429 OUTBOUND_LIMIT
6. قوائم الاستبعاد لكل تطبيق
لكل تطبيق مُرسِل (مفتاح API، وإلا الحساب لعمليات الإرسال بعد تسجيل الدخول) قائمة استبعاد خاصة به. يستبعد POST /api/v1/send المستلمين المستبعدين ويسجّلهم بالحالة suppressed؛ وإذا كان الجميع مستبعدين، تستجيب الواجهة بـ 422 ALL_RECIPIENTS_SUPPRESSED. للإدخالات السبب hard_bounce أو policy أو complaint أو unsubscribe أو manual. القراءة باستخدام delivery.read، والتغيير باستخدام delivery.write؛ وتُسجَّل الإضافة والإزالة في سجل التدقيق. قائمة الاستبعاد على مستوى الخادم الخاصة بـ Reputation Shield منفصلة.
GET /api/v1/suppressions?q=gmx.de&reason=hard_bounce&limit=100
POST /api/v1/suppressions {"emails":["alt@kunde.example"],"expiresAtUtc":"2027-01-01T00:00:00Z","note":"Kunde gekündigt"}
DELETE /api/v1/suppressions/alt@kunde.example
# Eintrag / entry
{"email":"bad@hard.example","reason":"hard_bounce","bounceClass":"mailbox_unknown",
"diagnostic":"550 5.1.1 User unknown","sendId":"...","recipientId":"...",
"createdAtUtc":"2026-11-10T08:00:03Z","expiresAtUtc":"2027-11-10T08:00:03Z"}
7. تضمين أداة الحالة المصغّرة (رمز الأداة، الأصول المسموح بها)
يجلب خادمكم رمزًا مميزًا للأداة المصغّرة باستخدام مفتاح API (النطاق delivery.read، بدون جلسة تسجيل دخول) عبر POST /api/v1/delivery/widget-token: معرّف sendId أو batchId واحد بالضبط، وttlSeconds من 60 إلى 86400 (الافتراضي 3600)، وحتى 10 أصول مسموح بها (https، وhttp فقط لـ localhost؛ عند الحذف: أصول المفتاح). الرمز nxwt_... موقّع وللقراءة فقط وصالح لهذا الإرسال أو هذه الدفعة فقط. لا تعرض رموز الدفعات العناوين أبدًا، ولا يتوفر includeRecipients إلا مع sendId. الأداة المصغّرة مكوّن ويب بدون تبعيات (shadow DOM، بدون eval، بدون أنماط مضمّنة) وتعمل تحت سياسة Content-Security-Policy صارمة. تحمل الاستجابات ترويسات CORS فقط لأصول الرمز، وإلا 403 ORIGIN_NOT_ALLOWED. تُضبط الأصول على المفتاح باستخدام allowedOrigins عبر واجهة API الإدارية.
POST /api/v1/delivery/widget-token
{"batchId":"drinklist-2026-11","ttlSeconds":3600,"origins":["https://drinklist.example"]}
<script src="https://mail.example.com/shared/delivery-widget.js" defer></script>
<nextxch-delivery-status token="nxwt_..." batch="drinklist-2026-11" lang="de" theme="auto"></nextxch-delivery-status>
<style>
nextxch-delivery-status { --nextxch-accent: #2563b0; --nextxch-radius: 12px; }
@media (prefers-color-scheme: dark) { nextxch-delivery-status { --nextxch-accent: #7fb2f0; } }
</style>
<script>
const el = document.querySelector('nextxch-delivery-status');
el.addEventListener('nextxch-delivery-error', async e => {
if (e.detail.code === 'WIDGET_TOKEN_EXPIRED') el.token = await fetch('/my-app/widget-token').then(r => r.text());
});
</script>
الإعدادات والقيم الافتراضية وأفضل الممارسات
| الإعداد / الموقع | الوصف | القيمة الافتراضية | أفضل ممارسة للإنتاج |
|---|---|---|---|
sendId, recipientId |
يعيّنه الخادم (UUID). مع idempotencyKey أو الترويسة Idempotency-Key يُرجع التكرار خلال 24 ساعة الاستجابة نفسها وsendId نفسه. لا تُتتبّع العناوين التي يوسّعها الخادم بنفسه (القوائم، إعادة التوجيه) كلٌّ على حدة. | - |
عيّنوا idempotencyKey لكل تشغيل عمل حتى لا يتسبب خطأ في الشبكة أثناء الإرسال في رسائل مكررة. |
batchId |
من 1 إلى 100 حرف (أحرف وأرقام و. _ : -، تبدأ بحرف أو رقم). تشكّل عدة عمليات إرسال بنفس batchId دفعة واحدة؛ ومعرّفات الدفعات خاصة بكل حساب. | bat_... (vom Server) |
المعرّفات ذات الدلالة مثل invoices-2026-11 تسهّل عوامل التصفية وعروض التقدّم. |
tags, metadata |
حتى 10 وسوم يتراوح طول كل منها من 1 إلى 64 حرفًا (تُخزَّن بأحرف صغيرة، والتصفية tag=)؛ وmetadata كأزواج مفتاح/قيمة حتى 2 KB، تُعاد في كل استجابة وحدث وخطاف ويب، ولا تُسجَّل أبدًا. | - |
لا تضعوا أسرارًا أو بيانات شخصية غير ضرورية في البيانات الوصفية. |
state (Empfängerzustand) |
الحالات scheduled وqueued وdeferred مفتوحة؛ والحالات delivered وbounced وsuppressed وexpired وcancelled نهائية. تكون حالة الإرسال pending ما دام هناك مستلم مفتوح، وإلا completed. | queued |
يحسب progressPercent في ملخص الدفعة المستلمين النهائيين؛ وتكون قيمة completed هي true عندما يكون الجميع في حالة نهائية. |
throttleReason |
السبب أثناء الانتظار: greylisting وrate_limit وprovider_cooldown وprovider_quota وconnection وtls_required وtemporary. | - |
يُظهر retry.byReason وretry.byProvider في ملخص الدفعة أي مزوّد يُبطئ الإرسال ومتى يُستأنف. |
bounceClass |
mailbox_unknown وmailbox_full وspam_rejected وpolicy وdns_failure وtls_required وtemporary. تُحدَّد من النص ومن رمز الحالة المحسّن (مثل x.1.1 وx.2.2 وx.7.10) ومن 4xx/5xx كحل احتياطي؛ ويُذكر بشكل منفصل ما إذا كان الارتداد دائمًا. | - |
ضعوا علامة على عناوين mailbox_unknown بوصفها غير صالحة في تطبيقكم بدلًا من إعادة المحاولة معها. |
DeliveryInsight:Verp |
مسار الإرجاع VERP local+nxb-<token>@domain (رمز من 34 حرفًا مشتق من recipientId وHMAC). يطابق تقارير التسليم (DSN) والارتدادات النصية الحرة والارتدادات المتأخرة مع مستلم واحد بالضبط. ينطبق فقط على نطاقات المرسل المحلية التي لا يتجاوز الجزء المحلي فيها 64 حرفًا. | true |
أبقِه مفعّلًا. بدون VERP تُطابَق التقارير وفق Message-ID ونطاق المرسل والمستلم، وذلك فقط خلال 7 أيام من آخر محاولة. |
Sperrliste je Anwendung (automatische Einträge) |
ارتداد دائم mailbox_unknown (السبب hard_bounce)، ورفض دائم متكرر بسبب السياسة (policy)، وشكوى ARF (complaint، 180 يومًا)، وإلغاء الاشتراك بنقرة واحدة وفق RFC 8058 (unsubscribe، بلا حد زمني). لا يُحتسب أبدًا الرفض الناتج عن DMARC أو SPF أو DKIM أو الحجم أو جهة المرسل. | hard_bounce 365 Tage · policy nach 2 Treffern 30 Tage |
اضبطوا المدد باستخدام DeliveryInsight:HardBounceSuppressionDays وPolicyBounceSuppressAfter وPolicySuppressionDays. |
Rate Limits (DeliveryInsight:*) |
300 طلب في الدقيقة لكل مفتاح (لكل عقدة)، و5 تدفقات مفتوحة لكل مفتاح، والأداة المصغّرة 300 في الدقيقة لكل رمز مميز، و3 تدفقات للأداة لكل رمز مميز و200 لكل مفتاح. الاستجابة 429 RATE_LIMITED مع Retry-After أو 429 TOO_MANY_STREAMS. | RequestsPerMinute 300 · MaxStreamsPerKey 5 |
استخدموا البث أو خطافات الويب بدلًا من الاستعلام المتكرر على فترات قصيرة. |
Aufbewahrung (RetentionDays, StreamRetentionHours) |
عمليات الإرسال والمستلمون والسجل 30 يومًا، والأحداث اللازمة لاستئناف البث 72 ساعة؛ ثم تُحذف بعد ذلك. نافذة الاستعلام بدون since: آخر 24 ساعة. | 30 Tage · 72 h |
انسخوا النتائج التي تحتاجونها لمدة أطول إلى قاعدة بياناتكم الخاصة عبر خطاف الويب. |
Webmail & Admin |
في Webmail يعرض مجلد البريد المرسل حالة التسليم لعمليات الإرسال الخاصة بكم عبر API (Free: آخر 24 ساعة). وفي الإدارة تعرض الصفحة #/delivery جميع عمليات الإرسال مع عوامل تصفية حسب الحساب والمفتاح والدفعة والوسم (Business؛ ويرى مسؤولو المستأجرين مستأجرهم). | - |
تستخدم عمليات الإيقاف وإعادة المحاولة وإعادة التوجيه في الإدارة وWebmail استدعاءات /api/v1 نفسها وتُسجَّل بالطريقة نفسها. |