Delivery and operations
The caller receives an accepted response. Delivery happens after that, on the worker. Operators watch the result. They do not sit on the provider’s HTTP call.
States
The platform state machine is explicit. Handlers do not invent status strings.
Received
Accepted
Rejected
Rendering
RenderFailed
QueuedForDelivery
Delivering
Sent
RetryScheduled
Failed
DeadLettered
Suppressed
Cancelled
Typical path: Accepted, Rendering, QueuedForDelivery, Delivering, Sent. A transient provider failure goes to RetryScheduled and later back to QueuedForDelivery. Exhausted retries become DeadLettered. A suppression match can move an accepted message to Suppressed. A scheduled message can be Cancelled before it sends.
Retries
The default schedule is safe and configurable.
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
The worker claims due work from Postgres with a lease (ClaimedUntil, FOR UPDATE SKIP LOCKED). Two workers do not send the same due row.
Routing and failover
MursilProviderRouter prefers the application default connection, then purpose, region, and tenant default or failover. Dispatch can re-route with the same eligibility rules. The policy used for a delivery is stored on MessageDelivery.ProviderRoutingPolicy.
What operators do with a failure
| Action | Route | Who |
|---|---|---|
| List triage | GET /api/v1/mursil/messages/deliveries/triage | support |
| Open one delivery | GET /api/v1/mursil/messages/deliveries/{deliveryId} | support |
| Retry | POST .../retry | operators |
| Suppress | POST .../suppress | operators |
| Resolve | POST .../resolve | operators |
The portal page for this queue is under Operations, below Trace. An empty capture means no failed deliveries were returned. A rising failure rate or dead-letter backlog is the reason to open that page. See the screenshots in Admin portal.
Suppressions and preferences
Suppression rows are an exchange of hashed recipients, not raw addresses. Import JSON is an array of { scope, reason, recipientHash }. Export downloads the current rows.
Recipients manage marketing and non-critical transactional mail through a signed link, GET /p/{token} and POST /p/{token} on Server. The token is HMAC, via Mursil:Tracking. Security mail is not suppressed there. One-click unsubscribe is POST /api/v1/mursil/preferences/one-click.
Open and click tracking is off until an operator turns it on and saves. The endpoints are GET /t/o/{token} and GET /t/c/{token}.
Audit and events
Operator actions are audited. GET /api/v1/mursil/audit lists records for operators.
When message, template, provider, or suppression state changes, Mursil writes a communication.* integration event through the transactional outbox. Payloads are redacted. Attachment bytes stay in MessageAttachments. Accepted events omit the raw file. Mursil does not host the subscriber that delivers those events to Zapier or another broker. That companion is a separate project.
Inbound provider events
Providers can call back. POST /api/v1/mursil/providers/{providerConnectionId}/events expects X-Mursil-Provider-Signature and X-Mursil-Provider-Timestamp. The signing secret on the connection is a reference, the same env: or secret: form as the send credential, and it is optional. A missing webhook signing secret is an honest local state, not a healthy production sender.
Inbound replies from a mailbox are a different route: authenticated POST /api/v1/mursil/messages/replies. Mursil does not host a mailbox. Outbound SMTP and HTTP providers can set Reply-To.