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_type | description | trigger |
|---|---|---|
user_otp_signup | A 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_reset | A 6-digit one-time passcode that lets a user reset their password. | POST /identities/users/password/forgot (Identities::Users::PasswordsController#forgot) |
admin_invite | A 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_created | Confirms 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_approved | Notifies the campaign point of contact that an admin approved the campaign, with the placement schedule summary. | PATCH /team/ads/campaigns/:id/approve |
ads_campaign_rejected | Notifies the campaign point of contact that an admin rejected the campaign, with the admin's notes. | PATCH /team/ads/campaigns/:id/reject |
careers_application_submitted | Notifies the hiring company that a candidate applied for one of their job listings. | POST /candidates/careers/job_applications |
careers_application_stage_updated | Notifies the candidate that the employer changed their application stage (e.g. shortlisted, rejected, hired). | PATCH /employers/careers/job_applications/:id |
jodgig_work_timing_adjustments | A 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_type | description | trigger |
|---|---|---|
billing_invoice_issued | Sends 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 readingv: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_typeand (where applicable) a stable foreign key likev:invoice_uuidorv: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_typevalues. 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_statuson stale events (events whose timestamp is older than the most recent applied event).
References
- Mailgun webhook docs: https://documentation.mailgun.com/docs/mailgun/user-manual/webhooks/webhooks
- Mailgun events list: https://documentation.mailgun.com/docs/mailgun/user-manual/events/events
- Billing invoice spec (example webhook handler):
30-49-domains/40-billing/models/billing-invoice.md(UC-2b, UC-2c) - Billing decision note on invoice email flow:
30-49-domains/40-billing/billing-notes/invoice-ref-number-ownership.md