#/delivery
Delivery Insight: estado de entrega por destinatario mediante API REST, stream en vivo, webhooks y widget
Delivery Insight muestra a una aplicación que envía a través de nextxch (POST /api/v1/send) el estado en vivo de sus propios correos hasta cada destinatario: entregado, en tránsito, en reintento con motivo y próximo intento, rebotado, suprimido o caducado. Incluye una API de estado, resumen del lote, stream en vivo (Server-Sent Events), webhooks firmados, intervenciones en la cola (detener, reintentar, reprogramar, redirigir), clasificación de rebotes con una lista de supresión por aplicación y un widget de estado integrable. URL base https://<server>/api/v1, legible por máquina como OpenAPI 3.1 en /api/v1/openapi.json. Forma parte de la API pública y, por tanto, está disponible a partir de Business.
Privacidad: Delivery Insight solo informa de lo que ocurre entre los servidores de correo. No hay seguimiento de aperturas ni de clics, ni píxeles de seguimiento ni enlaces reescritos. Los metadatos solo vuelven a usted y nunca se registran; los eventos del stream se conservan 72 horas, los envíos y el historial 30 días. Una clave de usuario solo ve los correos de su cuenta, una clave de inquilino solo lo que ella misma envió; los ids ajenos responden 404 igual que los desconocidos.
1. Inicio rápido: crear una clave, enviar, consultar el estado
Admin > API e integraciones > claves de integración: alcances mail.send y delivery.read (más webhooks.manage para webhooks y delivery.write para intervenciones y listas de supresión) y una dirección de remitente propia para la aplicación. La clave smk_... se muestra una sola vez. Como alternativa, un token OAuth 2.0 de POST /api/v1/oauth/token (client credentials, válido durante 1 hora).
# 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 de estado y resumen del lote
GET /api/v1/sends/{sendId} devuelve el envío con todos los destinatarios (attempts, nextAttemptAtUtc, lastSmtpCode, lastSmtpResponse, targetMx, tls, throttleReason, provider). GET /api/v1/sends enumera primero los más recientes, como máximo hasta el límite de retención (30 días); limit predeterminado 50, como máximo 200, continuar con cursor = pagination.nextCursor. GET /api/v1/batches/{batchId} resume todos los envíos de un lote para una indicación de progreso.
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. Historial y stream en vivo (SSE) con Last-Event-ID
Cada cambio de estado es un evento delivery.queued, delivery.deferred, delivery.delivered, delivery.bounced, delivery.suppressed, delivery.expired, delivery.cancelled o delivery.redirected con un número correlativo seq. El historial GET /api/v1/events devuelve primero los más antiguos (hasta 30 días, limit hasta 500). El stream GET /api/v1/events/stream envía id, event y data; cada 5 segundos un comentario (: ping) mantiene abiertos los proxies. Reanude con el encabezado Last-Event-ID (o ?lastEventId=) para eventos de hasta 72 horas atrás; el servidor sugiere retry: 3000. Las claves de API no deben estar en los navegadores: utilice allí el widget de estado.
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. Registrar webhooks y verificar la firma
POST /api/v1/webhooks (alcance webhooks.manage; para eventos de entrega, además delivery.read) responde 201 con el secreto de firma whsec_..., solo en esta respuesta. Los eventos de entrega deben indicarse explícitamente; * no los incluye. batchSeconds de 1 a 60 agrupa los eventos en un array JSON (X-Nextxch-Event: delivery.batch, como máximo 500); 0 envía cada uno por separado. Firma: X-Nextxch-Signature = v1= + hex(HMAC-SHA256(secret, X-Nextxch-Timestamp + . + raw body)). Utilice siempre el cuerpo sin procesar, compare en tiempo constante y rechace las marcas de tiempo antiguas (de más de 5 minutos). Responda con 2xx en un plazo de 10 segundos; de lo contrario, nextxch reintenta después de 30 s, 2 min, 10 min, 1 h y 6 h con el mismo id de evento.
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. Intervenciones: detener, reintentar, reprogramar, redirigir
Alcance delivery.write. Afecta solo a los destinatarios abiertos (scheduled, queued, deferred); los demás se omiten con el motivo final, in_flight o not_in_queue. El reintento y la reprogramación se aplican por elemento de la cola; todos los destinatarios abiertos del mismo elemento se mueven con él. Reprogramación como máximo 30 días hacia adelante, llamadas por lote de hasta 10.000 destinatarios. La redirección crea un nuevo destinatario (evento delivery.redirected, como máximo 3 redirecciones en cadena, cuenta para los límites de salida). Cada intervención se registra en el registro de auditoría de seguridad (DeliveryInsight.Cancel, .Retry, .Reschedule, .Redirect) con recuentos e ids, nunca con direcciones.
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. Listas de supresión por aplicación
Cada aplicación de envío (la clave de API o, en los envíos con sesión iniciada, la cuenta) tiene su propia lista de supresión. POST /api/v1/send omite los destinatarios suprimidos y los registra con el estado suppressed; si todos están suprimidos, la API responde 422 ALL_RECIPIENTS_SUPPRESSED. Las entradas tienen el motivo hard_bounce, policy, complaint, unsubscribe o manual. Lectura con delivery.read, modificación con delivery.write; las altas y liberaciones se registran en el registro de auditoría. La lista de supresión de todo el servidor del Reputation Shield es independiente.
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. Integrar el widget de estado (token de widget, orígenes permitidos)
Su servidor obtiene un token de widget con la clave de API (alcance delivery.read, sin sesión iniciada) mediante POST /api/v1/delivery/widget-token: exactamente un sendId o batchId, ttlSeconds de 60 a 86400 (predeterminado 3600), hasta 10 orígenes permitidos (https; http solo para localhost; si se omiten: los orígenes de la clave). El token nxwt_... está firmado, es de solo lectura y solo es válido para ese envío o lote. Los tokens de lote nunca muestran direcciones; includeRecipients solo existe con sendId. El widget es un componente web sin dependencias (shadow DOM, sin eval, sin estilos en línea) y funciona bajo una Content-Security-Policy estricta. Las respuestas llevan encabezados CORS solo para los orígenes del token; de lo contrario, 403 ORIGIN_NOT_ALLOWED. Los orígenes de la clave se definen con allowedOrigins mediante la API de administración.
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>
Configuración, valores predeterminados y buenas prácticas
| Ajuste / ubicación | Descripción | Valor predeterminado | Buena práctica de producción |
|---|---|---|---|
sendId, recipientId |
Asignado por el servidor (UUID). Con idempotencyKey o el encabezado Idempotency-Key, una repetición dentro de las 24 horas devuelve la misma respuesta y el mismo sendId. Las direcciones que el propio servidor expande (listas, reenvíos) no se siguen individualmente. | - |
Defina un idempotencyKey por ejecución de negocio para que un error de red durante el envío no provoque correos duplicados. |
batchId |
De 1 a 100 caracteres (letras, dígitos, . _ : -, empezando por una letra o un dígito). Varios envíos con el mismo batchId forman un lote; los ids de lote son por cuenta. | bat_... (vom Server) |
Los ids descriptivos como invoices-2026-11 facilitan los filtros y las indicaciones de progreso. |
tags, metadata |
Hasta 10 etiquetas de 1 a 64 caracteres (se guardan en minúsculas, filtro tag=); metadata como pares clave/valor de hasta 2 KB, devueltos en cada respuesta, evento y webhook, nunca registrados. | - |
No incluya secretos ni datos personales innecesarios en los metadatos. |
state (Empfängerzustand) |
scheduled, queued y deferred están abiertos; delivered, bounced, suppressed, expired y cancelled son finales. El estado de un envío es pending mientras haya un destinatario abierto; de lo contrario, completed. | queued |
progressPercent del resumen del lote cuenta los destinatarios finalizados; completed es true cuando todos están finalizados. |
throttleReason |
Motivo durante la espera: greylisting, rate_limit, provider_cooldown, provider_quota, connection, tls_required, temporary. | - |
retry.byReason y retry.byProvider del resumen del lote muestran qué proveedor limita la tasa y cuándo continúa. |
bounceClass |
mailbox_unknown, mailbox_full, spam_rejected, policy, dns_failure, tls_required, temporary. Se determina a partir del texto, del código de estado ampliado (por ejemplo, x.1.1, x.2.2, x.7.10) y, como alternativa, de 4xx/5xx; si un rebote es permanente se indica por separado. | - |
Marque las direcciones mailbox_unknown como no válidas en su propia aplicación en lugar de reintentarlas. |
DeliveryInsight:Verp |
Ruta de retorno VERP local+nxb-<token>@domain (token de 34 caracteres a partir de recipientId y HMAC). Asigna los informes de entrega (DSN), los rebotes en texto libre y los rebotes tardíos a exactamente un destinatario. Solo se aplica a dominios de remitente locales con un máximo de 64 caracteres en la parte local. | true |
Manténgalo activado. Sin VERP, los informes se asignan por Message-ID, dominio del remitente y destinatario, y solo dentro de los 7 días posteriores al último intento. |
Sperrliste je Anwendung (automatische Einträge) |
Rebote permanente mailbox_unknown (motivo hard_bounce), rechazo permanente repetido por política (policy), queja ARF (complaint, 180 días) y baja con un clic según RFC 8058 (unsubscribe, ilimitado). Los rechazos causados por DMARC, SPF, DKIM, el tamaño o el lado del remitente nunca cuentan. | hard_bounce 365 Tage · policy nach 2 Treffern 30 Tage |
Ajuste los plazos con DeliveryInsight:HardBounceSuppressionDays, PolicyBounceSuppressAfter y PolicySuppressionDays. |
Rate Limits (DeliveryInsight:*) |
300 solicitudes por minuto y clave (por nodo), 5 streams abiertos por clave, widget 300 por minuto y token, 3 streams de widget por token y 200 por clave. Respuesta 429 RATE_LIMITED con Retry-After o 429 TOO_MANY_STREAMS. | RequestsPerMinute 300 · MaxStreamsPerKey 5 |
Utilice el stream o los webhooks en lugar de consultar a intervalos cortos. |
Aufbewahrung (RetentionDays, StreamRetentionHours) |
Envíos, destinatarios e historial 30 días, eventos para reanudar el stream 72 horas; después se eliminan. Ventana de consulta sin since: las últimas 24 horas. | 30 Tage · 72 h |
Copie los resultados que necesite durante más tiempo en su propia base de datos mediante webhook. |
Webmail & Admin |
En Webmail, la carpeta Enviados muestra el estado de entrega de sus propios envíos por API (Free: últimas 24 horas). En la administración, la página #/delivery muestra todos los envíos con filtros por cuenta, clave, lote y etiqueta (Business; los administradores de inquilino ven su inquilino). | - |
Detener, reintentar y redirigir en la administración y en Webmail utilizan las mismas llamadas /api/v1 y se registran de la misma manera. |