API Reference

Overview

SelfMX provides a Resend-compatible REST API for sending transactional emails (sending only, no receiving or webhooks yet). All endpoints use JSON for request and response bodies.

Authentication

All API requests require authentication via Bearer token:

curl https://mail.yourdomain.com/emails \
  -H "Authorization: Bearer re_xxxxxxxxxxxx"

API keys are created in the admin UI and use the Resend format: re_ followed by 28 random characters.

Endpoints

EndpointMethodAuthDescription
/llms.txtGETNoLLM-oriented API reference
/healthGETNoHealth check
/system/statusGETNoSystem status check
/system/versionGETNoVersion and build info
/system/logsGETAdminApplication logs
/emailsPOSTAPI KeySend email
/emails/{id}GETAPI KeyGet sent email
/emailsGETAPI KeyList sent emails
/emails/batchPOSTAPI KeySend batch emails
/domainsGETAPI KeyList domains
/domainsPOSTAPI KeyCreate domain
/domains/{id}GETAPI KeyGet domain
/domains/{id}DELETEAPI KeyDelete domain
/domains/{id}/verifyPOSTAPI KeyTrigger verification check
/domains/{id}/test-emailPOSTAPI KeySend test email
/tokens/meGETAPI KeyToken introspection
/api-keysGETAdminList API keys
/api-keysPOSTAdminCreate API key
/api-keys/revokedGETAdminList archived API keys
/api-keys/{id}DELETEAdminRevoke API key
/sent-emailsGETAdminList sent emails
/sent-emails/{id}GETAdminGet sent email details
/auditGETAdminAudit logs
/hangfireGETAdminBackground jobs dashboard

Send Email

Send a transactional email.

POST /emails

Request

{
  "from": "Sender Name <[email protected]>",
  "to": ["[email protected]"],
  "cc": ["[email protected]"],
  "bcc": ["[email protected]"],
  "reply_to": "[email protected]",
  "subject": "Email Subject",
  "html": "<p>HTML content</p>",
  "text": "Plain text content"
}

Fields

FieldTypeRequiredDescription
fromstringYesSender email (must be verified domain)
tostring or arrayYesRecipient email(s)
subjectstringYesEmail subject line
htmlstringNo*HTML body content
textstringNo*Plain text body
ccarrayNoCarbon copy recipients
bccarrayNoBlind carbon copy recipients
reply_tostringNoReply-to address

*At least one of html or text is required.

Response

{
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "object": "email"
}
FieldTypeDescription
idstringGUID that uniquely identifies the sent email in SelfMX
objectstringAlways "email"

This format is compatible with official Resend SDKs.

Example

curl -X POST https://mail.yourdomain.com/emails \
  -H "Authorization: Bearer re_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "[email protected]",
    "to": "[email protected]",
    "subject": "Hello from SelfMX",
    "html": "<h1>Welcome!</h1><p>Your first email from SelfMX.</p>"
  }'

Get Email

Retrieve a previously sent email by ID. Returns Resend-compatible fields.

GET /emails/{id}

Response

{
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "from": "[email protected]",
  "to": ["[email protected]"],
  "cc": ["[email protected]"],
  "bcc": ["[email protected]"],
  "reply_to": ["[email protected]"],
  "subject": "Hello from SelfMX",
  "html": "<p>HTML content</p>",
  "text": "Plain text content",
  "created_at": "2024-01-15T10:30:00Z",
  "last_event": null
}

Notes

  • Returns 404 if the email ID does not exist
  • Returns 403 if the API key does not have access to the domain used to send the email
  • Fields cc, bcc, reply_to, text, html, scheduled_at, and last_event are omitted from the response when null

List Emails

List sent emails with cursor-based pagination.

GET /emails

Query Parameters

ParameterTypeDefaultDescription
beforestring-Cursor: email ID to paginate before
afterstring-Cursor: email ID to paginate after
limitinteger20Items per page (1-100)

Response

{
  "data": [
    {
      "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "from": "[email protected]",
      "to": ["[email protected]"],
      "subject": "Hello from SelfMX",
      "created_at": "2024-01-15T10:30:00Z",
      "last_event": null
    }
  ],
  "has_more": true
}

Notes

  • Non-admin API keys only see emails sent from their authorized domains
  • Admin keys see all sent emails

Send Batch Emails

Send multiple emails in a single request.

POST /emails/batch

Request

An array of email objects (same schema as Send Email):

[
  {
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Hello Alice",
    "html": "<p>Hi Alice</p>"
  },
  {
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Hello Bob",
    "html": "<p>Hi Bob</p>"
  }
]

Headers

HeaderValuesDefaultDescription
x-batch-validationstrict, permissivestrictValidation mode
  • strict (default): All emails are validated before any are sent. If any email fails validation, no emails are sent.
  • permissive: Emails are sent individually. Failed emails are reported in the errors array but don’t prevent other emails from being sent.

Response (strict mode)

{
  "data": [
    { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" },
    { "id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" }
  ]
}

Response (permissive mode)

{
  "data": [
    { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }
  ],
  "errors": [
    { "index": 1, "message": "Domain not verified: other.com" }
  ]
}

Notes

  • In strict mode, validation errors return 400 or 422 with a single error response
  • In permissive mode, the response always returns 200 with both data and optional errors arrays

List Domains

Get all domains for the authenticated API key.

GET /domains

Response

{
  "data": [
    {
      "id": "d5f2a3b1-...",
      "name": "yourdomain.com",
      "status": "verified",
      "createdAt": "2024-01-15T10:30:00Z",
      "lastCheckedAt": "2024-01-15T10:35:00Z",
      "nextCheckAt": null
    }
  ]
}

Domain Status Values

StatusDescription
pendingDomain added, waiting for setup job
verifyingDNS records created, waiting for verification
verifiedDomain verified and ready for sending
failedVerification failed

Create Domain

Add a domain for email sending.

POST /domains

Request

{
  "name": "example.com"
}

Response

{
  "id": "d5f2a3b1-...",
  "name": "example.com",
  "status": "pending",
  "createdAt": "2024-01-15T10:30:00Z",
  "lastCheckedAt": null,
  "nextCheckAt": null
}

Example

curl -X POST https://mail.yourdomain.com/domains \
  -H "Authorization: Bearer re_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"name": "example.com"}'

Get Domain

Get details for a specific domain including DNS records.

GET /domains/{id}

Response

{
  "id": "d5f2a3b1-...",
  "name": "example.com",
  "status": "verifying",
  "dnsRecords": [
    {
      "type": "CNAME",
      "name": "token1._domainkey.example.com",
      "value": "token1.dkim.amazonses.com"
    },
    {
      "type": "CNAME",
      "name": "token2._domainkey.example.com",
      "value": "token2.dkim.amazonses.com"
    },
    {
      "type": "CNAME",
      "name": "token3._domainkey.example.com",
      "value": "token3.dkim.amazonses.com"
    }
  ],
  "createdAt": "2024-01-15T10:30:00Z",
  "lastCheckedAt": "2024-01-15T10:32:00Z",
  "nextCheckAt": "2024-01-15T10:35:00Z"
}

Delete Domain

Remove a domain from SelfMX and AWS SES.

DELETE /domains/{id}

Response

204 No Content

Verify Domain

Manually trigger a verification check for a domain. Only works for domains in Verifying status.

POST /domains/{id}/verify

Response

Returns the updated domain with current verification status:

{
  "id": "d5f2a3b1-...",
  "name": "example.com",
  "status": "verified",
  "createdAt": "2024-01-15T10:30:00Z",
  "verifiedAt": "2024-01-15T10:35:00Z",
  "lastCheckedAt": "2024-01-15T10:35:00Z",
  "nextCheckAt": null
}

Example

curl -X POST https://mail.yourdomain.com/domains/{id}/verify \
  -H "Authorization: Bearer re_xxxxxxxxxxxx"

Notes

  • Returns 400 Bad Request if the domain is not in Verifying status
  • Use this to immediately check verification instead of waiting for the next scheduled check (every 5 minutes)
  • After verification succeeds, status becomes verified and nextCheckAt becomes null

Send Test Email

Send a test email from a verified domain. Useful for verifying domain configuration.

POST /domains/{id}/test-email

Request

{
  "senderPrefix": "test",
  "to": "[email protected]",
  "subject": "Test Email",
  "text": "This is a test email from SelfMX."
}

Fields

FieldTypeRequiredDescription
senderPrefixstringYesLocal part of sender address (e.g., test becomes [email protected])
tostringYesRecipient email address
subjectstringYesEmail subject line
textstringYesPlain text body content

Response

{
  "id": "msg_xxxxxxxxxxxx"
}

Example

curl -X POST https://mail.yourdomain.com/domains/{id}/test-email \
  -H "Authorization: Bearer re_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "senderPrefix": "test",
    "to": "[email protected]",
    "subject": "Test Email",
    "text": "This is a test email from SelfMX."
  }'

Notes

  • Domain must be in Verified status
  • Sender prefix must contain only alphanumeric characters, dots, underscores, and hyphens
  • The full sender address is constructed as {senderPrefix}@{domainName}

Token Introspection

Get effective permissions for the current authentication token. Useful for verifying token validity and discovering which domains an API key can access.

GET /tokens/me

Response

{
  "authenticated": true,
  "actorType": "api_key",
  "isAdmin": false,
  "name": "Production",
  "keyId": "k5f2a3b1-...",
  "keyPrefix": "re_abc123",
  "allowedDomainIds": ["d5f2a3b1-...", "d5f2a3b2-..."]
}

Fields

FieldTypeDescription
authenticatedbooleanAlways true for successful requests
actorTypestring"admin" for admin sessions/keys, "api_key" for regular keys
isAdminbooleanWhether the caller has admin privileges
namestringIdentity name (null for some auth types)
keyIdstringAPI key ID (present for API key auth)
keyPrefixstringAPI key prefix (present for API key auth)
allowedDomainIdsarrayDomain IDs this key can access (empty for admin)

Example

curl https://mail.yourdomain.com/tokens/me \
  -H "Authorization: Bearer re_xxxxxxxxxxxx"

Notes

  • Returns 401 if the token is invalid or missing
  • Admin sessions return actorType: "admin" with an empty allowedDomainIds array
  • keyId and keyPrefix are null when authenticated via cookie session

List API Keys (Admin)

Get all API keys. Requires admin authentication.

GET /api-keys

Response

{
  "data": [
    {
      "id": "k5f2a3b1-...",
      "name": "Production",
      "domains": ["example.com", "app.example.com"],
      "createdAt": "2024-01-15T10:30:00Z"
    }
  ]
}

Create API Key (Admin)

Create a new API key. Requires admin authentication.

POST /api-keys

Request

{
  "name": "Production",
  "domainIds": ["d5f2a3b1-...", "d5f2a3b2-..."]
}

Response

{
  "id": "k5f2a3b1-...",
  "name": "Production",
  "key": "re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "createdAt": "2024-01-15T10:30:00Z"
}

Important: The key field is only returned once at creation time. Store it securely.

Revoke API Key (Admin)

Revoke an API key. Requires admin authentication.

DELETE /api-keys/{id}

Response

204 No Content

List Archived API Keys (Admin)

Get archived (previously revoked) API keys. Revoked API keys are automatically archived after 90 days by a daily cleanup job. Requires admin authentication.

GET /api-keys/revoked

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger20Items per page

Response

{
  "data": [
    {
      "id": "k5f2a3b1-...",
      "name": "Old Production Key",
      "keyPrefix": "re_abc123",
      "isAdmin": false,
      "createdAt": "2024-01-15T10:30:00Z",
      "revokedAt": "2024-04-15T10:30:00Z",
      "archivedAt": "2024-07-15T04:00:00Z",
      "lastUsedAt": "2024-04-10T08:15:00Z",
      "domainIds": ["d5f2a3b1-...", "d5f2a3b2-..."]
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 5
}

Notes

  • Revoked keys are archived (moved to a separate table) after 90 days
  • The cleanup job runs daily at 4 AM UTC
  • Archived keys preserve historical data for audit purposes

List Sent Emails (Admin)

Get sent emails with cursor-based pagination and filtering. Requires admin authentication.

GET /sent-emails

Query Parameters

ParameterTypeDescription
domainIdstringFilter by domain ID
fromstringFilter by sender address (partial match)
tostringFilter by recipient address (partial match)
cursorstringCursor for next page
pageSizeintegerItems per page (default: 50)

Response

{
  "data": [
    {
      "id": "e5f2a3b1-...",
      "messageId": "msg_xxxxxxxxxxxx",
      "sentAt": "2024-01-15T10:30:00Z",
      "fromAddress": "[email protected]",
      "to": ["[email protected]"],
      "subject": "Hello from SelfMX",
      "domainId": "d5f2a3b1-...",
      "apiKeyId": "k5f2a3b1-...",
      "apiKeyName": "Production"
    }
  ],
  "nextCursor": "eyJpZCI6IjEyMyJ9",
  "hasMore": true
}

Pagination

Use cursor-based pagination for large datasets:

# First page
curl https://mail.yourdomain.com/sent-emails?pageSize=50

# Next page (use nextCursor from previous response)
curl https://mail.yourdomain.com/sent-emails?cursor=eyJpZCI6IjEyMyJ9

Get Sent Email (Admin)

Get details of a specific sent email including the full body. Requires admin authentication.

GET /sent-emails/{id}

Response

{
  "id": "e5f2a3b1-...",
  "messageId": "msg_xxxxxxxxxxxx",
  "sentAt": "2024-01-15T10:30:00Z",
  "fromAddress": "[email protected]",
  "to": ["[email protected]"],
  "cc": ["[email protected]"],
  "replyTo": "[email protected]",
  "subject": "Hello from SelfMX",
  "htmlBody": "<p>HTML content</p>",
  "textBody": "Plain text content",
  "domainId": "d5f2a3b1-...",
  "apiKeyId": "k5f2a3b1-...",
  "apiKeyName": "Production"
}

Notes

  • apiKeyId and apiKeyName identify which API key was used to send the email
  • apiKeyName is null if the API key has been deleted
  • Both fields are null for emails sent via admin session authentication

Audit Logs (Admin)

Get audit logs. Requires admin authentication.

GET /audit

Query Parameters

ParameterTypeDescription
fromdatetimeStart date (ISO 8601)
todatetimeEnd date (ISO 8601)
pageintegerPage number (default: 1)
pageSizeintegerItems per page (default: 50)

Response

{
  "data": [
    {
      "id": "a5f2a3b1-...",
      "apiKeyId": "k5f2a3b1-...",
      "action": "SendEmail",
      "details": "{...}",
      "createdAt": "2024-01-15T10:30:00Z"
    }
  ],
  "page": 1,
  "pageSize": 50,
  "totalCount": 1234
}

Error Responses

All errors return a Resend-compatible JSON format:

{
  "statusCode": 422,
  "name": "validation_error",
  "message": "Domain not verified: example.com",
  "error": {
    "code": "domain_not_verified",
    "message": "Domain not verified: example.com"
  }
}

Error Names

NameDescription
missing_api_keyNo API key provided
invalid_api_keyAPI key is invalid or revoked
invalid_accessKey doesn’t have access to the requested resource
validation_errorRequest validation failed (missing fields, unverified domain)
not_foundResource doesn’t exist
missing_required_fieldRequired request fields are missing
rate_limit_exceededToo many requests
internal_server_errorServer error

HTTP Status Codes

CodeDescription
400Bad Request - Invalid input or missing fields
401Unauthorized - Invalid or missing API key
403Forbidden - Key doesn’t have access to domain
404Not Found - Resource doesn’t exist
422Unprocessable Entity - Domain not verified
429Too Many Requests - Rate limit exceeded
500Internal Server Error

Rate Limits

API requests are rate limited per API key.

HeaderDescription
X-RateLimit-LimitRequests allowed per minute
X-RateLimit-RemainingRequests remaining
X-RateLimit-ResetUnix timestamp when limit resets

Default: 100 requests per minute per API key.

Health Check

Check if the API is running.

GET /health

Response

{
  "status": "Healthy"
}

System Status

Check system configuration and connectivity. Returns AWS and database health status. Use this to verify your deployment is properly configured.

GET /system/status

Response

{
  "healthy": true,
  "issues": [],
  "timestamp": "2026-02-02T10:30:00Z"
}

When configuration issues are detected:

{
  "healthy": false,
  "issues": [
    "AWS SES: Access Denied",
    "AWS: Region not configured (Aws__Region)"
  ],
  "timestamp": "2026-02-02T10:30:00Z"
}

Checks Performed

CheckDescription
AWS SESVerifies credentials can access SES account
DatabaseTests database connectivity
AWS ConfigValidates Region, AccessKeyId, SecretAccessKey are set

System Version

Get the API version and build information. Useful for debugging and verifying deployments.

GET /system/version

Response

{
  "version": "0.9.38.0",
  "informationalVersion": "0.9.38+3d4f16e",
  "buildDate": "2026-02-03T13:21:28Z",
  "environment": "Production"
}

Fields

FieldDescription
versionAssembly version (Major.Minor.Patch.Revision)
informationalVersionFull version including git commit hash
buildDateBuild timestamp (UTC)
environmentASP.NET environment (Development, Production)

System Logs (Admin)

Get recent application logs for remote diagnostics. Requires admin authentication.

GET /system/logs

Query Parameters

ParameterTypeDefaultDescription
countinteger1000Number of log entries to return (max 2000)
levelstring-Filter by log level (e.g., Error, Warning, Information)
categorystring-Filter by logger category (partial match)

Response

{
  "count": 150,
  "logs": [
    {
      "timestamp": "2026-02-03T14:21:05Z",
      "level": "Information",
      "category": "SelfMX.Api.Services.SesService",
      "message": "Email sent successfully to [email protected]",
      "exception": null
    },
    {
      "timestamp": "2026-02-03T14:20:58Z",
      "level": "Error",
      "category": "Microsoft.AspNetCore.Server.Kestrel",
      "message": "Connection reset by peer",
      "exception": "System.IO.IOException: Connection reset..."
    }
  ]
}

Example

# Get last 100 error logs
curl "https://mail.yourdomain.com/system/logs?count=100&level=Error" \
  -H "Cookie: auth_session=your_session_cookie"

# Get logs from a specific category
curl "https://mail.yourdomain.com/system/logs?category=SesService" \
  -H "Cookie: auth_session=your_session_cookie"

Notes

  • Logs are stored in memory (circular buffer of 2000 entries)
  • Logs are lost on application restart
  • Captures all log levels (Debug and above)
  • Useful for debugging issues without SSH access

Background Jobs Dashboard

View and manage Hangfire background jobs. Requires admin authentication.

GET /hangfire

Access the Hangfire dashboard to monitor:

  • Recurring jobs (domain verification polling)
  • Failed jobs and retry status
  • Job processing metrics
  • Queue status

The dashboard is available in all environments (development and production) and requires admin cookie authentication.