Sending mail
Integration is POST /api/v1/mursil/messages/email and the Mursil.Integration package. Production send uses a published template version. A draft is not live.
Request
POST /api/v1/mursil/messages/email
Authorization: Bearer <access-token>
X-Mursil-ApplicationId: 22222222-2222-4222-8222-222222222222
X-Mursil-ClientId: mursil-platform-service
Content-Type: application/json
{
"templateKey": "mursil.identity.password-reset",
"recipient": {
"email": "user@example.com",
"displayName": "Ayesha"
},
"locale": "en",
"model": {
"userName": "Ayesha",
"resetUrl": "https://identity.example.com/reset?token=example",
"expiresAt": "2026-05-15T12:00:00Z"
},
"idempotencyKey": "password-reset:user-123:2026-05-15T11:45Z",
"correlationId": "auth-flow-abc123"
}
| Field | Required | Meaning |
|---|---|---|
templateKey | yes | Published template key. |
recipient.email | yes | Recipient. Status responses do not echo it. |
recipient.displayName | no | Display name. |
locale | no | Requested locale. |
model | yes | Keys match the published schema. |
idempotencyKey | yes | One delivery per key. |
correlationId | no | Caller correlation for status lookup. |
notBefore | no | UTC time. The message stays accepted until then. |
attachments | no | File name, content type, and base64 bytes. Oversize or disallowed types are rejected before queueing. |
allowLocaleFallback | no | Default true. False returns not-found when that locale is unpublished. |
providerConnectionId | no | Optional provider guid. Empty keeps the router order. |
The API accepts the request and returns a message request id. It does not wait for the provider. A successful repeat of the same idempotency key returns the existing request and does not create a second delivery. The same key with a different payload is a conflict.
The first-party seed publishes mursil.email.raw-fallback with model fields subject and body. The same seed includes the Kayan identity keys (password-reset, email-verification, tenant-invitation, security-alert, passkey-alert, recovery). High-risk keys still need review before publish.
Status
GET /api/v1/mursil/messages/status?idempotencyKey=password-reset:user-123:2026-05-15T11:45Z
Pass the application and client headers, and exactly one of messageRequestId, idempotencyKey, or correlationId. Add X-Mursil-TenantId only when multi-tenancy is on.
MursilMessageStatusDto includes message request id, state, template key, template version id, provider kind, and a redacted failure. It has no rendered body and no raw recipient.
Related routes
| Method and path | Purpose |
|---|---|
POST /api/v1/mursil/messages/email/batch | Batch template send. |
POST /api/v1/mursil/messages/email/test | Test-send. |
POST /api/v1/mursil/messages/{messageRequestId}/cancel | Cancel a scheduled message. |
POST /api/v1/mursil/templates/preview | Render a preview. |
POST /api/v1/mursil/templates/lint | Lint template content. |
The full route index is on the Reference page. Public recipient routes are separate: /p/{token} for the preference center, /t/o/{token} for the open pixel, and /t/c/{token} for signed click redirects.
NuGet package
Package ids are Mursil.Integration and Mursil.Contracts, version 0.1.0-preview.1, on https://nuget.pkg.github.com/imabdul-dev/index.json.
AddMursilIntegration registers IEmailEngine, ICommunicationQueue, and ITransactionalEmailSender. SendAsync(TemplateEmailRequest) and EnqueueEmailAsync(QueuedEmailRequest) post the template key, locale, model, idempotency key, correlation id, and optional provider connection id. The queue returns Result<Guid>, the accepted message request id.
SendAsync(EmailRequest) with an empty template key sends RawEmailFallbackTemplateKey using subject and body. IsHtml is not a separate HTTP field. Older SMS queue methods return not-supported and do not call HTTP.
The package targets CleanArch Pro 0.1.0-preview.10. NuGet will not replace the existing 0.1.0-preview.1 package. This tree did not publish a newer version. DefaultTenantId is not required at startup. The package sends a tenant id only when Mursil:MultiTenancy is true.