9.6 Notification Retry Queue
Controller: NotificationRetryQueueController · Route: api/v1/NotificationRetryQueue ·
GroupName: notifications · 4 endpoints · Read-only visibility plus two operator actions over the
system-managed NotificationRetryQueue table — there is no Create/Update, since rows are only ever created
by the dispatch/retry engine itself. Full engine detail in Chapter 12.
| Verb + route | Permission | Request | Response (data) |
|---|---|---|---|
POST GetDataTable | NotificationRetryQueues.ViewAll | DtParameters | DtResult<NotificationRetryQueueDto> (unwrapped) |
GET GetDetails/{id} | NotificationRetryQueues.View | — | NotificationRetryQueueDetailsDto |
PUT RetryNow | NotificationRetryQueues.RetryNow | DeleteDto[] | Guid |
PUT Cancel | NotificationRetryQueues.Cancel | DeleteDto[] | Guid |
GET GetDetails/{id} — response shape
{
"succeeded": true,
"message": null,
"data": {
"id": "3e4f5a60-0000-0000-0000-000000000001",
"originalNotificationId": "8b1e2f40-0000-0000-0000-000000000001",
"channel": 1,
"recipient": "customer@example.com",
"eventKey": "PurchaseInvoicePosted",
"correlationId": "corr-9f1a2b3c",
"attemptNumber": 2,
"maxRetries": 5,
"state": 3,
"scheduledAt": "2026-07-04T10:15:00Z",
"processedAt": null,
"lastErrorCode": "SMTP_TIMEOUT",
"lastErrorMessage": "The SMTP server did not respond within the configured timeout.",
"retryStrategy": 2,
"nextDelaySeconds": 120,
"createdOn": "2026-07-04T10:11:00Z"
}
}
state (NotificationDeliveryState): Pending=0, Processing=1, Succeeded=2, RetryScheduled=3, Retrying=4, Failed=5, DeadLetter=6, Cancelled=7, Expired=8.
PUT RetryNow — force an immediate retry attempt
Request:
[ { "id": "3e4f5a60-0000-0000-0000-000000000001" } ]
Response:
{ "succeeded": true, "message": "Notification retry queue entry requeued for immediate processing", "data": "3e4f5a60-0000-0000-0000-000000000001" }
Business notes: sets ScheduledAt to now (or clears the claim) so the next notification-retry-processor-pipeline
Hangfire run (every 2 minutes) picks it up immediately instead of waiting for its computed backoff delay.
It does not bypass the retry engine's atomic claiming (ProcessingToken) — a row already being processed by
a concurrent worker run is not affected.
PUT Cancel
Request: same DeleteDto[] shape. Sets State = Cancelled — the row is retained for audit history (this
table uses soft-delete conventions but a cancelled retry is not the same as a deleted row) and will never be
picked up by the retry processor again, nor will it be escalated to a dead letter.
Angular: an operator "retry queue" dashboard — typically shows Pending/RetryScheduled/Retrying rows
with a countdown to ScheduledAt, and per-row "Retry Now"/"Cancel" buttons.
Backend: NotificationRetryProcessorJobAdapter → INotificationRetryWorker.ProcessDueRetriesAsync, run
by the Background Jobs Framework pipeline every 2 minutes — see Chapter 12.
Related APIs: Delivery Policies (retry rules),
Dead Letters (where exhausted retries land),
Analytics → RetryQueue (aggregate metrics).