अध्याय 15a क्लाइंट, ग्रुपवेयर & CLI एडमिन: #/delivery

Delivery Insight: REST API, लाइव स्ट्रीम, वेबहुक और विजेट के माध्यम से प्रति प्राप्तकर्ता डिलीवरी स्थिति

Delivery Insight किसी ऐसे एप्लिकेशन को जो nextxch के माध्यम से भेजता है (POST /api/v1/send) उसके अपने मेल की लाइव स्थिति हर प्राप्तकर्ता तक दिखाता है: डिलीवर, ट्रांज़िट में, कारण और अगले प्रयास के साथ पुनः प्रयास, bounced, suppressed या expired। इसमें स्टेटस API, बैच सारांश, लाइव स्ट्रीम (Server-Sent Events), हस्ताक्षरित वेबहुक, कतार में हस्तक्षेप (रोकें, पुनः प्रयास, पुनर्निर्धारण, redirect), प्रति एप्लिकेशन suppression सूची के साथ bounce वर्गीकरण और एक एम्बेड करने योग्य स्टेटस विजेट शामिल हैं। Base URL https://<server>/api/v1, /api/v1/openapi.json के अंतर्गत OpenAPI 3.1 के रूप में मशीन-पठनीय। सार्वजनिक API का हिस्सा और इसलिए Business से।

संचालन सुझाव और आर्किटेक्चर नोट

गोपनीयता: Delivery Insight केवल वही रिपोर्ट करता है जो मेल सर्वरों के बीच होता है। कोई ओपन ट्रैकिंग और कोई क्लिक ट्रैकिंग नहीं, कोई ट्रैकिंग पिक्सेल नहीं और कोई बदले गए लिंक नहीं। मेटाडेटा केवल आपको वापस भेजा जाता है और कभी लॉग नहीं किया जाता; स्ट्रीम के लिए ईवेंट 72 घंटे और भेजे गए मेल तथा इतिहास 30 दिन रखे जाते हैं। एक उपयोगकर्ता कुंजी केवल अपने खाते के मेल देखती है, एक टेनेंट कुंजी केवल वही जो उसने स्वयं भेजा; अन्य की ids अज्ञात ids की तरह 404 लौटाती हैं।

1. त्वरित शुरुआत: कुंजी बनाएँ, भेजें, स्थिति पढ़ें

एडमिन > API & Integrations > इंटीग्रेशन कुंजियाँ: स्कोप mail.send और delivery.read (साथ ही वेबहुक के लिए webhooks.manage और हस्तक्षेप तथा suppression सूचियों के लिए delivery.write) और एप्लिकेशन के लिए एक अलग प्रेषक पता। कुंजी smk_... केवल एक बार दिखाई जाती है। वैकल्पिक रूप से POST /api/v1/oauth/token से एक OAuth 2.0 टोकन (client credentials, 1 घंटे के लिए मान्य)।

संदर्भ / कॉन्फ़िगरेशन
# 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} सभी प्राप्तकर्ताओं के साथ send लौटाता है (attempts, nextAttemptAtUtc, lastSmtpCode, lastSmtpResponse, targetMx, tls, throttleReason, provider)। GET /api/v1/sends सबसे नए पहले सूचीबद्ध करता है, अधिकतम प्रतिधारण अवधि (30 दिन) तक पीछे; limit डिफ़ॉल्ट 50, अधिकतम 200, cursor = pagination.nextCursor के साथ जारी रखें। GET /api/v1/batches/{batchId} प्रगति प्रदर्शन के लिए बैच के सभी sends का योग देता है।

संदर्भ / कॉन्फ़िगरेशन
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. Last-Event-ID के साथ इतिहास और लाइव स्ट्रीम (SSE)

हर स्थिति परिवर्तन एक ईवेंट है: 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) प्रॉक्सी को खुला रखती है। 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. वेबहुक पंजीकृत करें और हस्ताक्षर सत्यापित करें

POST /api/v1/webhooks (स्कोप webhooks.manage, डिलीवरी ईवेंट के लिए अतिरिक्त रूप से delivery.read) 201 के साथ साइनिंग सीक्रेट whsec_... लौटाता है, केवल इसी उत्तर में। डिलीवरी ईवेंट को स्पष्ट रूप से नामित करना अनिवार्य है, * उन्हें शामिल नहीं करता। batchSeconds 1 से 60 ईवेंट को एक JSON array में बैच करता है (X-Nextxch-Event: delivery.batch, अधिकतम 500); 0 हर ईवेंट को अलग भेजता है। हस्ताक्षर: X-Nextxch-Signature = v1= + hex(HMAC-SHA256(secret, X-Nextxch-Timestamp + . + raw body))। हमेशा raw body का उपयोग करें, constant time में तुलना करें और पुराने टाइमस्टैम्प (5 मिनट से पुराने) अस्वीकार करें। 10 सेकंड के भीतर 2xx से उत्तर दें; अन्यथा nextxch उसी ईवेंट id के साथ 30 s, 2 min, 10 min, 1 h और 6 h के बाद पुनः प्रयास करता है।

संदर्भ / कॉन्फ़िगरेशन
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. हस्तक्षेप: रोकें, पुनः प्रयास, पुनर्निर्धारण, redirect

स्कोप delivery.write। केवल खुले प्राप्तकर्ताओं (scheduled, queued, deferred) को प्रभावित करता है; अन्य को कारण final, in_flight या not_in_queue के साथ छोड़ दिया जाता है। Retry और reschedule प्रति कतार आइटम लागू होते हैं, उसी आइटम के सभी खुले प्राप्तकर्ता साथ चलते हैं। Reschedule अधिकतम 30 दिन आगे तक, बैच कॉल अधिकतम 10,000 प्राप्तकर्ताओं तक। Redirect एक नया प्राप्तकर्ता बनाता है (ईवेंट delivery.redirected, एक श्रृंखला में अधिकतम 3 redirect, आउटबाउंड सीमाओं में गिना जाता है)। हर हस्तक्षेप सुरक्षा ऑडिट लॉग में दर्ज होता है (DeliveryInsight.Cancel, .Retry, .Reschedule, .Redirect) गिनती और ids के साथ, कभी पतों के साथ नहीं।

संदर्भ / कॉन्फ़िगरेशन
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. प्रति एप्लिकेशन suppression सूचियाँ

हर भेजने वाले एप्लिकेशन (API कुंजी, अन्यथा साइन-इन करके भेजे गए मेल के लिए खाता) की अपनी suppression सूची होती है। POST /api/v1/send suppressed प्राप्तकर्ताओं को छोड़ देता है और उन्हें स्थिति suppressed के साथ दर्ज करता है; यदि सभी suppressed हैं, तो API 422 ALL_RECIPIENTS_SUPPRESSED उत्तर देता है। प्रविष्टियों का कारण hard_bounce, policy, complaint, unsubscribe या manual होता है। delivery.read से पढ़ें, delivery.write से बदलें; जोड़ना और हटाना ऑडिट लॉग में दर्ज होता है। Reputation Shield की सर्वर-व्यापी suppression सूची अलग है।

संदर्भ / कॉन्फ़िगरेशन
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. स्टेटस विजेट एम्बेड करें (विजेट टोकन, अनुमत origins)

आपका सर्वर API कुंजी (स्कोप delivery.read, साइन-इन सत्र के बिना) से POST /api/v1/delivery/widget-token के माध्यम से एक विजेट टोकन प्राप्त करता है: ठीक एक sendId या batchId, ttlSeconds 60 से 86400 (डिफ़ॉल्ट 3600), अधिकतम 10 अनुमत origins (https, http केवल localhost के लिए; न देने पर: कुंजी के origins)। टोकन nxwt_... हस्ताक्षरित, केवल-पठन और केवल इसी send या बैच के लिए मान्य है। बैच टोकन कभी पते नहीं दिखाते, includeRecipients केवल sendId के साथ उपलब्ध है। विजेट बिना निर्भरताओं वाला एक वेब कॉम्पोनेंट है (shadow DOM, कोई eval नहीं, कोई inline styles नहीं) और सख्त Content-Security-Policy के अंतर्गत चलता है। उत्तरों में CORS हेडर केवल टोकन के origins के लिए होते हैं, अन्यथा 403 ORIGIN_NOT_ALLOWED। कुंजी के origins एडमिन API के माध्यम से allowedOrigins से सेट किए जाते हैं।

संदर्भ / कॉन्फ़िगरेशन
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 वाले कई sends एक बैच बनाते हैं; बैच ids प्रति खाता होती हैं। bat_... (vom Server) invoices-2026-11 जैसी सार्थक ids फ़िल्टर और प्रगति प्रदर्शन को आसान बनाती हैं।
tags, metadata 1 से 64 वर्णों के अधिकतम 10 टैग (छोटे अक्षरों में संग्रहीत, फ़िल्टर tag=); metadata 2 KB तक के key/value युग्मों के रूप में, हर उत्तर, ईवेंट और वेबहुक में लौटाया जाता है, कभी लॉग नहीं किया जाता। - मेटाडेटा में सीक्रेट या अनावश्यक व्यक्तिगत डेटा न डालें।
state (Empfängerzustand) scheduled, queued और deferred खुले हैं; delivered, bounced, suppressed, expired और cancelled अंतिम हैं। किसी send की स्थिति तब तक 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। टेक्स्ट, enhanced status code (उदाहरण के लिए x.1.1, x.2.2, x.7.10) और फ़ॉलबैक के रूप में 4xx/5xx से निर्धारित; कोई bounce स्थायी है या नहीं, यह अलग से बताया जाता है। - mailbox_unknown पतों को पुनः प्रयास करने के बजाय अपने एप्लिकेशन में अमान्य के रूप में चिह्नित करें।
DeliveryInsight:Verp VERP रिटर्न पाथ local+nxb-<token>@domain (recipientId और HMAC से 34-वर्णों का टोकन)। डिलीवरी रिपोर्ट (DSN), फ़्री-टेक्स्ट bounces और देर से आने वाले bounces को ठीक एक प्राप्तकर्ता से मिलाता है। केवल उन स्थानीय प्रेषक डोमेन पर लागू होता है जिनके local part में अधिकतम 64 वर्ण हों। true इसे चालू रखें। VERP के बिना रिपोर्ट Message-ID, प्रेषक डोमेन और प्राप्तकर्ता के आधार पर मिलाई जाती हैं, और केवल अंतिम प्रयास के 7 दिनों के भीतर।
Sperrliste je Anwendung (automatische Einträge) स्थायी mailbox_unknown bounce (कारण 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 कम अंतराल पर polling के बजाय स्ट्रीम या वेबहुक का उपयोग करें।
Aufbewahrung (RetentionDays, StreamRetentionHours) Sends, प्राप्तकर्ता और इतिहास 30 दिन, स्ट्रीम फिर से शुरू करने के लिए ईवेंट 72 घंटे; उसके बाद हटा दिए जाते हैं। since के बिना क्वेरी अवधि: पिछले 24 घंटे। 30 Tage · 72 h जिन परिणामों की आपको अधिक समय तक आवश्यकता है, उन्हें वेबहुक के माध्यम से अपने डेटाबेस में कॉपी करें।
Webmail & Admin वेबमेल में भेजे गए फ़ोल्डर आपके अपने API sends की डिलीवरी स्थिति दिखाता है (Free: पिछले 24 घंटे)। एडमिन में पेज #/delivery खाता, कुंजी, बैच और टैग के अनुसार फ़िल्टर के साथ सभी sends दिखाता है (Business; टेनेंट व्यवस्थापक अपना टेनेंट देखते हैं)। - एडमिन और वेबमेल में रोकना, पुनः प्रयास और redirect समान /api/v1 कॉल का उपयोग करते हैं और उसी तरह लॉग किए जाते हैं।