Deliveries
Re-deliver a stored message to its webhook. See Delivery & reliability for a guided walk-through.
Auth: Session token · Base URL:
https://api.mailkite.dev
Endpoints
Each row expands to its request and response schema. The address bar follows along, so any endpoint can be linked to directly.
POST /api/deliveries/:id/retry Re-deliver a stored message to its webhook.
Re-deliver a stored message to its webhook.
Response body
retry-delivery-response.jsonPOST /api/deliveries/:id/retry — the outcome and the NEW delivery row the retry appended (history is append-only; the original row is untouched).
| Field | Type | Notes |
|---|---|---|
| ok req | boolean | The endpoint accepted the redelivery. |
| status req | integer | HTTP status the endpoint returned (0 when the request failed before a response). |
| delivery req | delivery | The new delivery row recording this retry. |
| delivery.id req | string | Delivery id (dlv_…). |
| delivery.message_id req | string | The message this delivery carries (msg_…). |
| delivery.route_id req | string | The route whose webhook was targeted (rte_…). |
| delivery.url req | string | The webhook URL the body was POSTed to. |
| delivery.status req | "pending" · "delivered" · "failed" · "paused" | Current state of this delivery. |
| delivery.attempts req | integer | How many attempts this delivery has made. |
| delivery.last_status_code | integer · null | HTTP status of the most recent attempt. |
| delivery.next_retry_at | integer · null | When the auto-retry cron will re-attempt a failed delivery; null when nothing is due, in Unix epoch milliseconds (UTC); null when it has never happened. |
| delivery.retry_attempts | integer | How many automatic retries have been scheduled so far. |
| delivery.created_at req | integer | When the delivery was created, in Unix epoch milliseconds (UTC). |
| delivery.updated_at req | integer | When the delivery last changed, in Unix epoch milliseconds (UTC). |
An actual response, recorded from the conformance suite.
{
"ok": true,
"status": 200,
"delivery": {
"id": "dlv_7Mn3PxQb",
"message_id": "msg_2Hk9QpVn4tLd",
"route_id": "rte_4Bn8XcVm",
"url": "https://myapp.ai/hooks/mailkite",
"status": "delivered",
"attempts": 2,
"last_status_code": 200,
"created_at": 1769817600000,
"updated_at": 1769817600000
}
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/retry-delivery-response.json",
"title": "Retry delivery response",
"description": "POST /api/deliveries/:id/retry — the outcome and the NEW delivery row the retry appended (history is append-only; the original row is untouched).",
"type": "object",
"required": [
"ok",
"status",
"delivery"
],
"properties": {
"ok": {
"type": "boolean",
"description": "The endpoint accepted the redelivery."
},
"status": {
"type": "integer",
"description": "HTTP status the endpoint returned (0 when the request failed before a response).",
"examples": [
200
]
},
"delivery": {
"$ref": "delivery.json",
"description": "The new delivery row recording this retry."
}
}
} POST /api/deliveries/retry Replay a whole selection of webhook deliveries in one call — the bulk form of retryDelivery. Pass `deliveryIds` (replay those exact deliveries), `messageIds` (replay each message's most recent delivery per route), and/or `threadIds` (expanded server-side to every message in the conversation); they combine. At most 50 ids per request, so send larger selections as sequential batches. Always answers 200 with a per-id `results` array — one unreachable endpoint never hides the outcomes of the rest — so branch on `ok` and `results`, not the HTTP status. An id you don't own is reported as skipped with reason `not_found`, exactly as the single-delivery endpoint 404s.
Replay a whole selection of webhook deliveries in one call — the bulk form of retryDelivery. Pass `deliveryIds` (replay those exact deliveries), `messageIds` (replay each message's most recent delivery per route), and/or `threadIds` (expanded server-side to every message in the conversation); they combine. At most 50 ids per request, so send larger selections as sequential batches. Always answers 200 with a per-id `results` array — one unreachable endpoint never hides the outcomes of the rest — so branch on `ok` and `results`, not the HTTP status. An id you don't own is reported as skipped with reason `not_found`, exactly as the single-delivery endpoint 404s.
Request body
replay-request.jsonWhich stored deliveries to re-POST. Pass at least one of `deliveryIds`, `messageIds`, or `threadIds` — they combine, and the same endpoint is never hit twice for one message in a single call. At most 50 ids per request (threads are expanded to their messages BEFORE the cap applies); send larger selections as sequential batches.
| Field | Type | Notes |
|---|---|---|
| deliveryIds | string[] | Replay exactly these delivery rows (dlv_…), to the URL each recorded. |
| messageIds | string[] | For each message (msg_…), replay its most recent delivery per route. A message no webhook ever fired for is reported as skipped with reason `no_delivery`. |
| threadIds | string[] | Conversation ids — expanded server-side to every message in the thread, then treated as `messageIds`. Lets a thread-grouped list select whole conversations. |
The smallest body that makes this call — every other field is optional.
{
"deliveryIds": [
"dlv_1",
"dlv_2",
"dlv_gone"
]
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/replay-request.json",
"title": "Replay request",
"description": "Which stored deliveries to re-POST. Pass at least one of `deliveryIds`, `messageIds`, or `threadIds` — they combine, and the same endpoint is never hit twice for one message in a single call. At most 50 ids per request (threads are expanded to their messages BEFORE the cap applies); send larger selections as sequential batches.",
"type": "object",
"properties": {
"deliveryIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Replay exactly these delivery rows (dlv_…), to the URL each recorded.",
"examples": [
[
"dlv_6Kp2LsWq",
"dlv_9Xr4TmBd"
]
]
},
"messageIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "For each message (msg_…), replay its most recent delivery per route. A message no webhook ever fired for is reported as skipped with reason `no_delivery`.",
"examples": [
[
"msg_2Hk9QpVn4tLd"
]
]
},
"threadIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Conversation ids — expanded server-side to every message in the thread, then treated as `messageIds`. Lets a thread-grouped list select whole conversations.",
"examples": [
[
"msg_2Hk9QpVn4tLd"
]
]
}
}
} Response body
replay-response.jsonThe outcome of a batch replay. Always 200 with per-id detail — one unreachable endpoint must not hide the outcomes of the rest — so check `ok` and `results`, not the HTTP status.
| Field | Type | Notes |
|---|---|---|
| ok req | boolean | Every requested id succeeded. False if anything failed or was skipped. |
| requested req | integer | How many replays this call accounted for — equal to `results.length`. |
| delivered req | integer | How many endpoints accepted the replay. |
| failed req | integer | How many were attempted and rejected (or timed out). |
| skipped req | integer | How many ids were never attempted — unresolvable, or a message with no delivery to replay. |
| results req | replay-result[] | One row per requested id, in no guaranteed order. |
| results[].messageId req | string · null | The message replayed (msg_…); null when the id could not be resolved to one. |
| results[].routeId req | string · null | The route whose webhook was targeted (rte_…); null when nothing was attempted. |
| results[].deliveryId req | string · null | The NEW delivery row this replay appended (dlv_…); null when nothing was attempted. |
| results[].ok req | boolean | The endpoint accepted this replay. |
| results[].status req | integer · null | HTTP status the endpoint returned; null when the request failed before a response, or nothing was attempted. |
| results[].reason req | string · null | Why it did not succeed. `not_found` = no such id on this account (a foreign id reads the same way, deliberately). `no_delivery` = the message has no webhook delivery to replay. Otherwise the delivery's own classification: `http`, `timeout`, `network`, `redirect`, or `no-ack`. Null when ok. |
An actual response, recorded from the conformance suite.
{
"ok": false,
"requested": 3,
"delivered": 1,
"failed": 1,
"skipped": 1,
"results": [
{
"messageId": "msg_2Hk9QpVn4tLd",
"routeId": "rte_4Bn8XcVm",
"deliveryId": "dlv_7Mn3PxQb",
"ok": true,
"status": 200,
"reason": null
},
{
"messageId": "msg_5Tq1RvXz",
"routeId": "rte_4Bn8XcVm",
"deliveryId": "dlv_9Wc4KdHp",
"ok": false,
"status": 500,
"reason": null
},
{
"messageId": null,
"routeId": null,
"deliveryId": "dlv_gone",
"ok": false,
"status": null,
"reason": "not_found"
}
]
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/replay-response.json",
"title": "Replay response",
"description": "The outcome of a batch replay. Always 200 with per-id detail — one unreachable endpoint must not hide the outcomes of the rest — so check `ok` and `results`, not the HTTP status.",
"type": "object",
"required": [
"ok",
"requested",
"delivered",
"failed",
"skipped",
"results"
],
"properties": {
"ok": {
"type": "boolean",
"description": "Every requested id succeeded. False if anything failed or was skipped."
},
"requested": {
"type": "integer",
"description": "How many replays this call accounted for — equal to `results.length`.",
"examples": [
12
]
},
"delivered": {
"type": "integer",
"description": "How many endpoints accepted the replay.",
"examples": [
10
]
},
"failed": {
"type": "integer",
"description": "How many were attempted and rejected (or timed out).",
"examples": [
1
]
},
"skipped": {
"type": "integer",
"description": "How many ids were never attempted — unresolvable, or a message with no delivery to replay.",
"examples": [
1
]
},
"results": {
"type": "array",
"items": {
"$ref": "replay-result.json"
},
"description": "One row per requested id, in no guaranteed order."
}
}
} GET /api/deliveries/:id/attempts Every captured attempt for one delivery, newest first: the request headers and payload we POSTed, and the status, headers, and body that came back. This is the per-attempt record behind a webhook post-mortem, fetched without pulling the whole message. Captures are retained for 45 days.
Every captured attempt for one delivery, newest first: the request headers and payload we POSTed, and the status, headers, and body that came back. This is the per-attempt record behind a webhook post-mortem, fetched without pulling the whole message. Captures are retained for 45 days.
Response body
delivery-attempts-response.jsonEvery captured attempt for one delivery, newest first — the request we sent and the response we got back. Captures are retained for 45 days.
| Field | Type | Notes |
|---|---|---|
| attempts req | delivery-attempt[] | Attempts for this delivery, newest first. |
| attempts[].id req | string | Attempt id (att_…). |
| attempts[].delivery_id req | string | The delivery this attempt belongs to (dlv_…). |
| attempts[].message_id req | string | The message delivered (msg_…). |
| attempts[].attempt_number req | integer | 1-based attempt counter within the delivery. |
| attempts[].ok req | 0 · 1 | 1 when the attempt succeeded. |
| attempts[].reason | string · null | Failure classification when not ok (e.g. 'http_5xx', 'timeout', 'no-ack'). |
| attempts[].request_headers | string · null | Request headers we sent, as a JSON string (content-type, x-mailkite-signature, x-mailkite-timestamp). Null for attempts recorded before capture existed. |
| attempts[].request_body | string · null | The JSON payload we POSTed, truncated to ~4KB. Null for attempts recorded before capture existed. |
| attempts[].response_status | integer · null | HTTP status the endpoint returned. |
| attempts[].response_headers | string · null | Response headers as a JSON string. |
| attempts[].response_body | string · null | Response body, truncated to ~4KB. |
| attempts[].duration_ms | integer · null | Round-trip time of the attempt. |
| attempts[].requested_at req | integer | When the attempt started, in Unix epoch milliseconds (UTC). |
| attempts[].created_at req | integer | When the audit row was recorded, in Unix epoch milliseconds (UTC). |
An actual response, recorded from the conformance suite.
{
"attempts": [
{
"id": "att_3Yh7GbFd",
"delivery_id": "dlv_6Kp2LsWq",
"message_id": "msg_2Hk9QpVn4tLd",
"attempt_number": 2,
"ok": 1,
"reason": null,
"request_headers": "{\"content-type\":\"application/json\"}",
"request_body": "{\"type\":\"email.received\"}",
"response_status": 200,
"response_headers": null,
"response_body": "ok",
"duration_ms": 142,
"requested_at": 1769817600000,
"created_at": 1769817600000
}
]
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/delivery-attempts-response.json",
"title": "Delivery attempts response",
"description": "Every captured attempt for one delivery, newest first — the request we sent and the response we got back. Captures are retained for 45 days.",
"type": "object",
"required": [
"attempts"
],
"properties": {
"attempts": {
"type": "array",
"items": {
"$ref": "delivery-attempt.json"
},
"description": "Attempts for this delivery, newest first."
}
}
}