#/delivery
Delivery Insight: REST API、ライブ ストリーム、Webhook、ウィジェットによる受信者ごとの配信状態
Delivery Insight は、nextxch 経由で送信するアプリケーション (POST /api/v1/send) に対し、自身が送信したメールのライブ状態を受信者単位で示します: 配信済み、配送中、理由と次回試行付きの再試行中、バウンス、抑制、期限切れ。ステータス API、バッチ サマリー、ライブ ストリーム (Server-Sent Events)、署名付き Webhook、キューへの介入 (停止、再試行、再スケジュール、リダイレクト)、アプリケーションごとの抑制リストを伴うバウンス分類、埋め込み可能なステータス ウィジェットを備えています。ベース URL は https://<server>/api/v1、機械可読な OpenAPI 3.1 は /api/v1/openapi.json にあります。公開 API の一部であるため、Business 以上で利用できます。
プライバシー: Delivery Insight が報告するのは、メール サーバー間で起きたことだけです。開封トラッキングやクリック トラッキング、トラッキング ピクセル、リンクの書き換えは一切ありません。メタデータはお客様にのみ返され、ログには記録されません。ストリーム用のイベントは 72 時間、送信データと履歴は 30 日間保持されます。ユーザー キーは自分のアカウントのメールのみ、テナント キーは自分が送信したもののみを参照できます。他者の ID には、不明な ID と同様に 404 が返されます。
1. クイック スタート: キーの作成、送信、ステータスの確認
管理画面 > API & 連携 > 連携キー: スコープ mail.send と delivery.read (Webhook には webhooks.manage、介入と抑制リストには delivery.write を追加) と、アプリケーション専用の送信者アドレス。キー smk_... は 1 回だけ表示されます。代わりに POST /api/v1/oauth/token の OAuth 2.0 トークン (クライアント資格情報、有効期間 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} は、すべての受信者 (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
すべての状態変化は、連番 seq 付きのイベント delivery.queued、delivery.deferred、delivery.delivered、delivery.bounced、delivery.suppressed、delivery.expired、delivery.cancelled、delivery.redirected のいずれかになります。履歴 GET /api/v1/events は古い順に返します (最大 30 日、limit は最大 500)。ストリーム GET /api/v1/events/stream は id、event、data を送信し、5 秒ごとのコメント (: ping) でプロキシの接続を維持します。最大 72 時間前までのイベントは、Last-Event-ID ヘッダー (または ?lastEventId=) で再開できます。サーバーは 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. Webhook の登録と署名の検証
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 分より前) は拒否してください。10 秒以内に 2xx で応答してください。応答がない場合、nextxch は同じイベント ID で 30 秒、2 分、10 分、1 時間、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) に件数と ID 付きで記録され、アドレスは記録されません。
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 として記録します。全員が抑制されている場合、API は 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 のどちらか 1 つだけを指定し、ttlSeconds は 60 から 86400 (既定値 3600)、許可するオリジンは最大 10 個です (https のみ、http は localhost の場合のみ。省略時はキーのオリジン)。トークン nxwt_... は署名済みで読み取り専用、この送信またはバッチに対してのみ有効です。バッチ トークンではアドレスは表示されず、includeRecipients は sendId でのみ使用できます。ウィジェットは依存関係のない Web コンポーネント (Shadow DOM、eval なし、インライン スタイルなし) で、厳格な Content-Security-Policy の下で動作します。応答にはトークンのオリジンに対してのみ CORS ヘッダーが付与され、それ以外は 403 ORIGIN_NOT_ALLOWED になります。キーのオリジンは管理 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 を持つ複数の送信で 1 つのバッチになります。バッチ ID はアカウントごとです。 | bat_... (vom Server) |
invoices-2026-11 のような意味のある ID を使うと、フィルターや進捗表示が容易になります。 |
tags, metadata |
1 から 64 文字のタグを最大 10 個 (小文字で保存、フィルター tag=)。metadata は最大 2 KB のキー/値ペアで、すべての応答、イベント、Webhook で返され、ログには記録されません。 | - |
メタデータにシークレットや不要な個人データを含めないでください。 |
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 (recipientId と HMAC から生成される 34 文字のトークン)。配信レポート (DSN)、自由形式のバウンス、遅延バウンスを正確に 1 人の受信者に照合します。ローカル パートが 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 のウィジェット ストリームまでです。超過時は Retry-After 付きの 429 RATE_LIMITED または 429 TOO_MANY_STREAMS が返されます。 | RequestsPerMinute 300 · MaxStreamsPerKey 5 |
短い間隔でのポーリングではなく、ストリームまたは Webhook を使用してください。 |
Aufbewahrung (RetentionDays, StreamRetentionHours) |
送信、受信者、履歴は 30 日、ストリーム再開用のイベントは 72 時間保持され、その後削除されます。since を指定しない場合の照会期間は直近 24 時間です。 | 30 Tage · 72 h |
長期間必要な結果は、Webhook を使って自社のデータベースにコピーしてください。 |
Webmail & Admin |
Webmail では、[送信済み] フォルダーに自分の API 送信の配信状態が表示されます (Free: 直近 24 時間)。管理画面では、#/delivery ページにすべての送信が表示され、アカウント、キー、バッチ、タグで絞り込めます (Business。テナント管理者には自分のテナントが表示されます)。 | - |
管理画面と Webmail の停止、再試行、リダイレクトは同じ /api/v1 呼び出しを使い、同じ方法でログに記録されます。 |