MailLandDocs

Retry Logic

Understand how Mail.Land retries failed webhook deliveries

When a webhook delivery fails, MailLand automatically retries with increasing delays. This page covers the retry schedule, delivery statuses, failure handling, and how to monitor and retry deliveries manually.

Retry Schedule

This section covers every event delivered by the durable dispatcher: message.*, suppression.*, account.* and inbound.received. See Inbound Deliveries below for what is specific to inbound events.

MailLand retries a failed delivery up to 6 times in total, with an increasing delay before each retry:

AttemptDelay before this attemptTotal Elapsed
1: (initial attempt)0
2Immediate~0
31 minute~1 minute
45 minutes~6 minutes
515 minutes~21 minutes
61 hour~1 hour 21 minutes

After the 6th attempt fails, the delivery is marked as permanently failed: there is no further wait and no 7th attempt.

Retryable vs Non-Retryable

Retryable responses

These status codes trigger automatic retries:

  • 408 Request Timeout
  • 429 Too Many Requests
  • 500 Internal Server Error
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout
  • Network errors (timeouts, socket errors, DNS failures)

Non-retryable responses

These status codes are treated as permanent failures, no retry:

  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • All other 4xx errors (except 408 and 429)
  • A redirect response that fails validation (see Redirects) or exceeds the hop limit

If your endpoint returns a Retry-After header on a retryable response, MailLand uses that value as the delay before the next attempt instead of the schedule above: for any retryable status, not only 429. There is no upper cap and no jitter applied to it: honor this on your side too if you send it, since a very large value is honored exactly as given.

Redirects

MailLand does not treat your endpoint as a browser would. Redirects are followed manually, at most 3 hops, and every hop is re-validated before anything is sent to it:

  • the target must be https://: a redirect to http:// is refused, not downgraded;
  • the target must resolve to a public address: a redirect to a private, loopback or link-local address is refused, and the request is never issued;
  • exceeding 3 hops fails the delivery.

A refused redirect is reported as a delivery failure with no status code. If your endpoint moved, point the webhook at the new URL rather than relying on a redirect.

Delivery Configuration

SettingDefaultDescription
HTTP timeout15 secondsMax wait time per delivery attempt
Max retries6 attemptsTotal delivery attempts including the initial one
Auto-pause threshold10 consecutive failuresWebhook is paused after this many failures in a row: uses the webhook's own pauseThreshold when set to a value greater than 0, otherwise this default
DeduplicationWhile the delivery record existsDuplicates of the same event are suppressed by the (WebhookId, EventId) constraint for as long as the row exists: at least 30 days for delivered deliveries

Inbound Deliveries

inbound.received is delivered by the same durable dispatcher as every other event, with the same retry schedule, Retry-After handling, delivery history and auto-pause described above.

  • Deduplicate on event_id (equal to X-MLND-Event-Id), like any other event. It is stable per inbound message and webhook, so every attempt for the same email carries the same id.
  • A manual replay is a new event: a new event_id, with the original one in data.replay_of. data.idempotency_key stays the same.
  • Each attachment's download_url is signed for each attempt and is valid for 15 minutes from then; the attachment's id is the same on every attempt. See Inbound Webhooks.

View Delivery History

See past deliveries for a webhook:

GET https://api.mail.land/v1/webhooks/{id}/deliveries
List delivery history
curl -X GET "https://api.mail.land/v1/webhooks/wh_abc123/deliveries?limit=20" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Query Parameters

ParameterTypeDefaultDescription
cursorstring-Opaque cursor from the previous page's nextCursor. Omit for the first page.
limitint20Items per page
statusstring-Filter: success or failed
eventTypestring-Filter by event type (e.g., message.delivered)
fromdatetime-Start date
todatetime-End date
Delivery history response
{
  "items": [
    {
      "publicId": "p8r2kx7m9wq4rtz",
      "eventType": "message.delivered",
      "messageId": "t_a1b2c3d4e5f6",
      "statusCode": 200,
      "responseTimeMs": 145,
      "success": true,
      "errorMessage": null,
      "createdAt": "2026-02-17T12:34:58.000Z",
      "attemptNumber": 1
    },
    {
      "publicId": "p8r2kx7m3nq1abc",
      "eventType": "message.bounced",
      "messageId": "t_d4e5f6a1b2c3",
      "statusCode": 503,
      "responseTimeMs": 15000,
      "success": false,
      "errorMessage": "Service Unavailable",
      "createdAt": "2026-02-17T12:30:00.000Z",
      "attemptNumber": 3
    }
  ],
  "nextCursor": "eyJ2IjoxLCJzIjoiY3JlYXRlZGF0OmRlc2MiLCJrIjoi...",
  "hasMore": true
}

Retry a Failed Delivery

Manually retry a specific failed delivery. The retry carries the same event_id and body as the original attempt, only X-MLND-Attempt and the signature timestamp differ, so your handler should deduplicate on event_id rather than assume a second delivery is a second event:

POST https://api.mail.land/v1/webhooks/{id}/deliveries/{publicId}/retry
Retry a delivery
curl -X POST https://api.mail.land/v1/webhooks/wh_abc123/deliveries/p8r2kx7m3nq1abc/retry \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Returns 202 Accepted on success. The retry is queued and uses the original event payload.

StatusCodeDescription
202-Retry queued successfully
202-Delivery is already queued for retry, no-op, not an error
400ALREADY_SUCCEEDEDCannot retry a delivery that already succeeded
400INVALID_REQUESTDelivery predates durable retry tracking, or is currently in flight
404WEBHOOK_NOT_FOUNDWebhook does not exist
404DELIVERY_NOT_FOUNDDelivery ID does not exist

Auto-Pause on Failures

When a webhook accumulates enough consecutive failed delivery attempts, MailLand automatically pauses it. The threshold is the webhook's own pauseThreshold when it is set to a value greater than 0, otherwise the default of 10 consecutive failed delivery attempts:

  1. The webhook's isActive is set to false, and pausedAt and pausedReason are populated: pausedReason tells you how many failures and what the last one was
  2. An in-app notification is created
  3. No further events are delivered until the webhook is resumed

The counter is of attempts, not of events: one event that exhausts its retry ladder contributes several failures.

There is no earlier warning notification before the threshold is hit: check consecutiveFailures proactively (see Best Practices below) rather than waiting to be told.

Resume a Paused Webhook

POST https://api.mail.land/v1/webhooks/{id}/resume
Resume a paused webhook
curl -X POST https://api.mail.land/v1/webhooks/wh_abc123/resume \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

This reactivates the webhook: it clears the pause state, resets the consecutive failure counter to zero, and sets it active again. The next event delivery will be attempted normally.

Any successful delivery automatically resets the consecutive failure counter to zero. A single success after 9 failures prevents the webhook from being paused.

Best Practices

  • Return 200 quickly: process webhook payloads asynchronously. Queue the event and return 200 immediately. Long-running handlers risk timeouts.
  • Use the event ID for idempotent consumption: the X-MLND-Event-Id header (and the matching event_id in the payload) is stable across retries and replays: the same underlying event always arrives with the same id, no matter how many times it is redelivered. Store processed IDs and skip ones you have already handled, so a duplicate delivery is a no-op on your side.
  • Monitor consecutive failures: check the consecutiveFailures field when listing webhooks. Investigate before hitting the auto-pause threshold.
  • Handle 429 properly: if your endpoint is overloaded, return 429 with a Retry-After header. MailLand will respect it.

Next Steps

On this page