Hogsend
Operating

Email Operations

Track every email from queue to delivery — monitor opens, clicks, bounces, and troubleshoot issues

Every email Hogsend sends is tracked in the database — from initial queue through delivery, opens, clicks, bounces, and complaints. Hogsend sends email through a swappable provider — Resend by default — but tracking, preferences, and the email_sends record are engine-owned, so the numbers below look the same no matter which provider delivers the mail. This page covers how to monitor email delivery, investigate issues, and maintain healthy deliverability.

Email Send History

List email sends with optional filters:

# All emails, most recent first
curl -H "Authorization: Bearer your-api-key" \
  http://localhost:3002/v1/admin/emails

# Emails to a specific recipient
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/emails?toEmail=user@acme.com"

# Emails using a specific template
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/emails?templateKey=activation/welcome"

# Failed emails in the last week
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/emails?status=failed&from=2026-05-18T00:00:00Z"

# Bounced emails
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/emails?status=bounced"

# Everything a single user has been sent, oldest first
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/emails?userId=user_abc123&sort=createdAt&order=asc"

# Emails from one journey that were opened
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/emails?journeyId=activation-welcome&engagement=opened"

# Most recently clicked emails across a template
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/emails?templateKey=conversion-trial-expiring&engagement=clicked&sort=clickedAt&order=desc"

Filters: toEmail, templateKey, category, status, journeyId, userId, engagement (opened / clicked / bounced / complained), and a from/to window on createdAt. engagement matches on the underlying timestamp column rather than the latest status, so a filter like engagement=opened still catches an email that has since clicked or bounced. Sort with sort (createdAt, sentAt, openedAt, clickedAt) and order (asc / desc); the default is newest-first by createdAt. journeyId and userId are resolved from the email's linked journey state.

{
  "emails": [
    {
      "id": "email-uuid",
      "journeyStateId": "state-uuid",
      "templateKey": "activation/welcome",
      "messageId": "provider-message-id",
      "resendId": "provider-message-id",
      "fromEmail": "noreply@hogsend.com",
      "toEmail": "user@acme.com",
      "subject": "Welcome to Hogsend",
      "category": "journey",
      "status": "delivered",
      "userId": "user_abc123",
      "journeyId": "activation-welcome",
      "sentAt": "2025-01-15T10:30:00.000Z",
      "deliveredAt": "2025-01-15T10:30:05.000Z",
      "openedAt": "2025-01-15T11:00:00.000Z",
      "clickedAt": null,
      "bouncedAt": null,
      "complainedAt": null,
      "createdAt": "2025-01-15T10:30:00.000Z",
      "updatedAt": "2025-01-15T11:00:00.000Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

messageId is the provider message id (Resend email_id, Postmark MessageID, …); it replaced resendId, which lingers as a @deprecated alias for one minor — both fields are returned and carry the same value during the deprecation window, so always read messageId going forward.

Email Status Lifecycle

An email moves through these statuses as delivery progresses:

queued -> rendered -> sent -> delivered -> opened -> clicked
                        \-> bounced
                        \-> complained
                \-> failed
StatusMeaning
queuedEmail task created, waiting for worker to pick it up
renderedTemplate rendered successfully, ready to send
sentHanded off to your email provider (Resend by default) for delivery
deliveredThe provider confirmed the email reached the recipient's mail server
openedRecipient opened the email (tracked via pixel)
clickedRecipient clicked a tracked link
bouncedEmail bounced (hard bounce -- invalid address, mailbox does not exist)
complainedRecipient marked the email as spam
failedTemplate rendering or the provider send call failed

First-party open/click tracking is the single source of truth — the engine rewrites links and injects the open pixel itself before handing the HTML to the provider, so opened/clicked are recorded the same way regardless of provider. delivered, bounced, and complained come from the provider's webhook, normalized to a provider-neutral EmailEvent. Open and click tracking still depends on the recipient's email client -- some clients block tracking pixels or pre-fetch links, so these numbers are lower bounds.

Email Detail View

Get the full detail for a single email, including tracked link clicks and the journey context:

curl -H "Authorization: Bearer your-api-key" \
  http://localhost:3002/v1/admin/emails/email-uuid
{
  "email": {
    "id": "email-uuid",
    "journeyStateId": "state-uuid",
    "templateKey": "activation/welcome",
    "messageId": "provider-message-id",
    "resendId": "provider-message-id",
    "fromEmail": "noreply@hogsend.com",
    "toEmail": "user@acme.com",
    "subject": "Welcome to Hogsend",
    "category": "journey",
    "status": "delivered",
    "sentAt": "2025-01-15T10:30:00.000Z",
    "deliveredAt": "2025-01-15T10:30:05.000Z",
    "openedAt": "2025-01-15T11:00:00.000Z",
    "clickedAt": null,
    "bouncedAt": null,
    "complainedAt": null
  },
  "events": [
    { "type": "queued", "timestamp": "2025-01-15T10:30:00.000Z" },
    { "type": "sent", "timestamp": "2025-01-15T10:30:01.000Z" },
    { "type": "delivered", "timestamp": "2025-01-15T10:30:05.000Z" },
    { "type": "opened", "timestamp": "2025-01-15T11:00:00.000Z" },
    {
      "type": "clicked",
      "timestamp": "2025-01-15T11:05:00.000Z",
      "url": "https://example.com/docs",
      "ipAddress": "192.168.1.1",
      "userAgent": "Mozilla/5.0..."
    }
  ],
  "trackedLinks": [
    {
      "id": "link-uuid",
      "originalUrl": "https://example.com/docs",
      "clickCount": 3,
      "clicks": [
        {
          "id": "click-uuid",
          "clickedAt": "2025-01-15T11:05:00.000Z",
          "ipAddress": "192.168.1.1",
          "userAgent": "Mozilla/5.0..."
        }
      ]
    }
  ],
  "journeyContext": {
    "journeyId": "activation-welcome",
    "userId": "user_abc123",
    "status": "completed",
    "currentNodeId": "done"
  }
}

The journeyContext field is null if the email was not sent from a journey. The trackedLinks array shows every link that was tracked in the email, with individual click records including timestamps, IP addresses, and user agents.

The events array is the same history pre-assembled into a single chronological (ascending) timeline — one entry per lifecycle moment (queued, sent, delivered, opened, bounced, complained, and failed when a send fails) plus one clicked entry per recorded link click (carrying its url, ipAddress, and userAgent). It's the easiest thing to render in a per-contact activity view.

Understanding the Delivery Timeline

The events array above gives you the ready-made timeline; the raw timestamps on the email object are the underlying source:

TimestampWhat happened
createdAtEmail send record created in the database
sentAtEmail handed to your email provider's API
deliveredAtThe provider confirmed delivery to the recipient's mail server
openedAtRecipient opened the email
clickedAtRecipient clicked a tracked link

The gap between sentAt and deliveredAt is the delivery latency. This is typically under 10 seconds. If you see consistently large gaps, check your provider's sender reputation and DNS configuration.

Email Metrics by Template

See how each template is performing:

curl -H "Authorization: Bearer your-api-key" \
  http://localhost:3002/v1/admin/metrics/emails
{
  "templates": [
    {
      "templateKey": "activation/welcome",
      "sent": 480,
      "delivered": 475,
      "opened": 320,
      "clicked": 150,
      "bounced": 5,
      "deliveryRate": 0.99,
      "openRate": 0.6737,
      "clickRate": 0.4688,
      "clickToDeliveryRate": 0.3158
    },
    {
      "templateKey": "activation/getting-started",
      "sent": 300,
      "delivered": 298,
      "opened": 150,
      "clicked": 45,
      "bounced": 2,
      "deliveryRate": 0.99,
      "openRate": 0.5034,
      "clickRate": 0.30,
      "clickToDeliveryRate": 0.1510
    }
  ]
}

Scope it with a from/to window (on createdAt), and pass includeUntemplated=true to fold raw/sendRaw sends (no templateKey) into a "(none)" bucket:

curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/metrics/emails?from=2026-05-01T00:00:00Z&to=2026-06-01T00:00:00Z"

openRate is opens ÷ delivered, but falls back to opens ÷ sent when no deliveries are recorded yet (so opens aren't silently zeroed if your provider's delivered webhook isn't wired up). clickToDeliveryRate (clicks ÷ delivered) is the most robust headline number when open tracking is partially blocked by mail clients.

Benchmarks

Use these as rough guidelines for SaaS lifecycle emails:

MetricGoodNeeds attentionInvestigate
Delivery rate>98%95-98%<95%
Open rate>50%30-50%<30%
Click rate>15%5-15%<5%
Bounce rate<1%1-3%>3%

Low open rates usually indicate subject line or timing issues. Low click rates suggest the email content or CTA is not compelling. High bounce rates point to stale contact data.

Track deliverability over time to spot degradation early:

# Daily deliverability for the last 30 days
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/metrics/emails/deliverability?period=day&from=2026-04-25T00:00:00Z"

# Weekly trends
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/metrics/emails/deliverability?period=week&from=2026-01-01T00:00:00Z"

# Monthly overview
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/metrics/emails/deliverability?period=month&from=2025-01-01T00:00:00Z"
{
  "points": [
    {
      "date": "2026-05-24",
      "total": 120,
      "delivered": 118,
      "bounced": 1,
      "complained": 1,
      "deliveryRate": 0.983
    },
    {
      "date": "2026-05-25",
      "total": 95,
      "delivered": 94,
      "bounced": 1,
      "complained": 0,
      "deliveryRate": 0.989
    }
  ]
}

Watch for:

  • Delivery rate dropping below 95% -- may indicate DNS/SPF/DKIM issues, blacklisting, or sudden contact quality problems
  • Bounce spikes -- could mean a bad batch import or stale data
  • Complaint increases -- review email frequency and content. Complaints above 0.1% are a sender reputation risk.

Resending Failed Emails

If an email failed due to a transient issue (provider API timeout, temporary rendering error), you can retry it:

curl -X POST http://localhost:3002/v1/admin/emails/email-uuid/resend \
  -H "Authorization: Bearer your-api-key"
{
  "emailId": "email-uuid",
  "status": "queued"
}

Only emails in failed or bounced status can be resent. The email must have a templateKey so it can be re-rendered with the original data. Attempting to resend an email in any other status returns 409 Conflict.

Before resending a bounced email, check whether the recipient's address is actually valid. Resending to an invalid address will bounce again and further damage your sender reputation.

Bounce Tracking and Suppression

Hogsend automatically tracks bounces via your provider's webhooks, normalized to a provider-neutral EmailEvent. Each bounce carries a BounceClass that decides what happens:

  • permanent (hard bounce — invalid address, mailbox does not exist): the email is marked bounced, the contact's bounceCount is incremented, and once bounceCount reaches the threshold (default: 3) the contact is suppressed (suppressed: true).
  • complaint: immediate suppression.
  • transient (soft bounce — mailbox full, temporary failure): recorded but never suppresses.
  • unknown: recorded conservatively, never suppresses.

So for a permanent bounce:

  1. The email record's status is updated to bounced with a bouncedAt timestamp
  2. The contact's bounceCount is incremented in their email preferences
  3. If bounceCount reaches the threshold (default: 3), the contact is suppressed (suppressed: true)

Suppressed contacts:

  • Will not receive any emails from any journey
  • Will not pass the subscription check in entry guards
  • Remain in the system (not deleted) with all historical data intact

Checking Suppressed Contacts

# View a contact's bounce status
curl -H "Authorization: Bearer your-api-key" \
  http://localhost:3002/v1/admin/contacts/user_abc123/preferences

If suppressed: true and bounceCount >= 3, the contact was auto-suppressed due to bounces.

Un-suppressing a Contact

If a user confirms their email is valid (e.g., they contact support), you can un-suppress them:

curl -X PUT http://localhost:3002/v1/admin/contacts/user_abc123/preferences \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "suppressed": false }'

Note that this does not reset the bounce count. If the email bounces again, the contact will be re-suppressed immediately. To also reset the bounce count, you will need to update the database directly.

Troubleshooting

Every email goes to one address

If all sends land in a single inbox with a [TEST → …] subject prefix, the instance is in test mode — the engine redirects every send while the sending domain is unverified. Verify it with hogsend domain check; sends go live within a minute of DNS verifying.

High Bounce Rates

If your bounce rate exceeds 3%:

  1. Check recent imports -- a bad import with stale email addresses is the most common cause
# Find recently imported contacts that bounced
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/emails?status=bounced&from=2026-05-20T00:00:00Z"
  1. Review the deliverability trend -- was it a sudden spike or gradual increase?
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/metrics/emails/deliverability?period=day&from=2026-05-01T00:00:00Z"
  1. Check DNS configuration -- verify SPF, DKIM, and DMARC records for your sending domain are correct in Resend

  2. Review contact sources -- if bounces are concentrated in contacts from a specific source, that source may need validation

Emails Not Being Delivered

If emails show sent status but never move to delivered:

  1. Check your provider dashboard -- the email may be queued on the provider's side (the Resend dashboard if you're on the default provider)
  2. Check the recipient's spam folder -- the email may have been delivered but classified as spam
  3. Verify provider webhooks are working -- the delivered status comes from your provider's webhook events (delivered to POST /v1/webhooks/email/:providerId, normalized to a provider-neutral EmailEvent). If the webhook is misconfigured, Hogsend will not know about delivery
# Check the health endpoint -- if Redis is down, webhook processing may be affected
curl http://localhost:3002/v1/health

Email Shows "failed" Status

Check the DLQ for the failure details:

curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/dlq?source=email&status=pending"

Common failure causes:

ErrorResolution
Resend API timeoutTransient -- retry via /resend endpoint
Template rendering errorBug in the email template code. Fix and redeploy.
Invalid email addressContact has a malformed email. Update the contact.
Resend API key invalidCheck the RESEND_API_KEY environment variable
Rate limited by ResendReduce sending volume or contact Resend for higher limits

For the full endpoint specification, see the API Reference. For per-template metrics and trends, see Metrics & Analytics.

On this page