Skip to main content

Mailgun — email types tracker

Jod sends all outbound transactional email through Mailgun. There is one Mailgun domain today — mg.jodapp.com — used for every email type.

Every email send must include the Mailgun custom variable v:email_type=<value>, where <value> is one of the rows in the table below. The webhook handler uses this tag to route delivery events to the right domain handler and to ignore events for unrelated email types.

Email types

email_typedescriptiontrigger
user_otp_signupA 6-digit one-time passcode that verifies a user's email during signup.POST /identities/users/generate_otp (Identities::UsersController#generate_otp)
user_otp_password_resetA 6-digit one-time passcode that lets a user reset their password.POST /identities/users/password/forgot (Identities::Users::PasswordsController#forgot)
admin_inviteA tokenised link that lets a Jod team admin set a password and access the Team Portal.Rake task bin/rails admins:invite (operations-only, not user-triggered)
ads_campaign_createdConfirms a new advertising campaign was created. Sent to the campaign's point of contact.POST /employers/ads/campaigns and POST /team/ads/campaigns
ads_campaign_approvedNotifies the campaign point of contact that an admin approved the campaign, with the placement schedule summary.PATCH /team/ads/campaigns/:id/approve
ads_campaign_rejectedNotifies the campaign point of contact that an admin rejected the campaign, with the admin's notes.PATCH /team/ads/campaigns/:id/reject
careers_application_submittedNotifies the hiring company that a candidate applied for one of their job listings.POST /candidates/careers/job_applications
careers_application_stage_updatedNotifies the candidate that the employer changed their application stage (e.g. shortlisted, rejected, hired).PATCH /employers/careers/job_applications/:id
jodgig_work_timing_adjustmentsA one-off policy update sent to legacy JodGig users about a work-timing adjustment policy change.Rake task bin/rails jod_gig:send_work_timing_adjustments (operations-only, not user-triggered)

Planned email types

Designed but not yet implemented. Move them into the main table when shipped.

email_typedescriptiontrigger
billing_invoice_issuedSends an issued invoice file (PDF) to the company's bill-to email. Drives the transition :draft → :issued.UC-2b "Send Invoice to Client" — admin clicks the action on a draft invoice in the Team Portal. See 30-49-domains/40-billing/models/billing-invoice.md.

Notes on the current table

OTP mailer is reused across two flows

Identities::Users::OtpMailer#send is invoked from two different managers — one for signup verification, one for password reset. The class and template are the same, but the funnel they belong to differs. We track them as separate email_type values so Mailgun analytics can distinguish acquisition from retention.

ads_campaign_created has two trigger paths

The same email body is sent from both employer self-serve (POST /employers/ads/campaigns) and admin-on-behalf-of (POST /team/ads/campaigns) flows. The subject line varies based on who created the campaign, but the email is semantically the same event. We track it as one email_type. If Mailgun analytics later need to split actor types, add a separate v:created_by_type custom variable rather than splitting the email type.

Operations-only email types

admin_invite and jodgig_work_timing_adjustments are triggered only via rake tasks. They have no controller endpoint and no request context. Bear this in mind when adding tracking or auditing that assumes every email originates from an HTTP request.

Architecture rules

These rules apply to every email type Jod sends through Mailgun, not just one domain.

  • One Mailgun domain (mg.jodapp.com) for all email types. Webhook scoping is done inside the Rails handler by reading v:email_type, not by separating Mailgun domains. Separate domains add DNS overhead, sender-reputation warmup, and operational complexity without a matching benefit at Jod's current volume.
  • Every send call must include v:email_type and (where applicable) a stable foreign key like v:invoice_uuid or v:campaign_uuid. Use the UUID, not the primary key — webhook payloads are stored by Mailgun and may appear in logs, so an enumerable integer leaks volume information. Consider a wrapper service that requires these as keyword arguments so they are hard to forget.
  • Verify the Mailgun webhook HMAC signature on every incoming payload. Reject unsigned or invalid-signature payloads with HTTP 401.
  • Return HTTP 200 OK for unknown or unhandled email_type values. A non-200 response causes Mailgun to retry, which is wasteful.
  • Webhook handlers must be idempotent. Dedupe by Mailgun's event identifier. Do not downgrade delivery_status on stale events (events whose timestamp is older than the most recent applied event).

References