Every applicant-tracking system — Greenhouse, Lever, Workday, Ashby, SmartRecruiters, iCIMS — sends the candidate a stream of status emails as an application moves through the pipeline: “Application received”, “Phone screen scheduled”, “Onsite interview”, “Offer”, “Rejection”. Each ATS lays the message out differently, but a job-tracker, a recruiting-analytics dashboard, or a candidate-experience tool always needs the same handful of fields: what stage the application is at, which role and company, at which ATS, who the recruiter is, and when the next thing happens. MailFrame extracts those fields from the notification email into typed, schema-validated JSON you can route straight into an application tracker, a stage-change webhook, or an offer-alerting workflow.
It works on the body of application-status emails from every major ATS —
Greenhouse (no-reply@greenhouse.io and tenant *.greenhouse-mail.io
addresses), Lever (no-reply@hire.lever.co), Workday (tenant
*.myworkday.com addresses), Ashby (notifications@ashbyhq.com),
SmartRecruiters, and iCIMS. Match on the message body and the ATS-specific
wording (“we received your application”, “your interview is scheduled”, “an
offer is on its way”) rather than the From address alone — companies relay
these through tenant-specific and white-labeled careers domains. The
application stage, role, company, ATS provider, job posting ID, recruiter,
event date, and next step all normalize into one consistent shape regardless of
which system sent the message. PDF, image, and calendar-attachment input are on
the roadmap; inbox forwarding is planned.
Fields MailFrame extracts
| Field | Type | Example | Notes |
|---|---|---|---|
notification_type | enum | application_received | One of application_received, screening_scheduled, interview_scheduled, interview_completed, offer_extended, offer_accepted, offer_declined, rejected, withdrawn |
application_id | string | 88472013 | The ATS’s internal application/candidate ID, when present — the dedup key across a candidate’s lifecycle |
job_title | string | Senior Backend Engineer | The role as written on the posting |
company_name | string | Acme Co | The hiring company — the employer, not the ATS vendor |
company_domain | string | acme.example | Sending or careers domain |
ats_provider | enum | greenhouse | One of greenhouse, lever, workday, ashby, smartrecruiters, icims, unknown |
job_posting_id | string | 4025001 | The vendor’s external job ID — Greenhouse’s numeric job ID, Lever’s UUID, Workday’s jobRequisitionId |
job_location | string | Remote (US) | Posting location — e.g. Remote, New York, NY |
recruiter_name | string | Priya Shah | Recruiter or coordinator named on the email |
recruiter_email | string | priya.shah@acme.example | Recruiter’s reply-to address when present |
hiring_manager_name | string | Dev Patel | Hiring manager named on the email, when present |
event_date | string | 2026-07-10T15:00:00-04:00 | Date/time of the interview, offer, or rejection, ISO 8601 |
event_location | string | Acme HQ, 500 Market St, San Francisco, CA | Physical address or Remote for a scheduled interview |
event_location_url | string | https://meet.google.com/abc-defg-hij | Conferencing link for a remote interview when present |
next_step | string | Our recruiting team will review your application and be in touch. | Free-text next-action description from the email |
status_url | string | https://boards.greenhouse.io/acme/jobs/4025001/status | Link to the candidate’s application status page |
date | string | 2026-07-03 | Date the email was sent, normalized to ISO 8601 |
Sample input
A typical Greenhouse “application received” email looks like this:
From: Acme Co Recruiting <no-reply@greenhouse.io>
Subject: We received your application for Senior Backend Engineer
Date: Thu, 03 Jul 2026 09:12:00 -0400
To: jordan.rivera@example.com
Hi Jordan,
Thanks for applying to the Senior Backend Engineer role at Acme Co. Our
recruiting team has received your application and will review it shortly.
Position: Senior Backend Engineer
Location: Remote (US)
Requisition: 4025001
Recruiter: Priya Shah <priya.shah@acme.example>
We'll be in touch about next steps. In the meantime, you can check the
status of your application any time.
View application:
https://boards.greenhouse.io/acme/jobs/4025001/status
The same shape is produced for Lever, Workday, Ashby, SmartRecruiters, and
iCIMS status emails — notification_type distinguishes an
application_received acknowledgement from a screening_scheduled or
interview_scheduled invite (which carry event_date, event_location, and
event_location_url), an interview_completed recap, an offer_extended
notice, an offer_accepted / offer_declined confirmation, a rejected
decision, or a withdrawn update — and the rest of the fields normalize to the
same JSON keys regardless of which ATS sent the message.
Structured JSON output
{
"notification_type": "application_received",
"application_id": "88472013",
"job_title": "Senior Backend Engineer",
"company_name": "Acme Co",
"company_domain": "acme.example",
"ats_provider": "greenhouse",
"job_posting_id": "4025001",
"job_location": "Remote (US)",
"recruiter_name": "Priya Shah",
"recruiter_email": "priya.shah@acme.example",
"next_step": "Our recruiting team will review your application and be in touch about next steps.",
"status_url": "https://boards.greenhouse.io/acme/jobs/4025001/status",
"date": "2026-07-03"
}
JSON Schema definition
Every field is validated against the schema before MailFrame returns it.
You can copy this as a starting point and tighten it for your own use
case — for example, require event_date if every notification in your
workflow is an interview invite, or restrict notification_type to
{"offer_extended", "rejected"} if you only route terminal decisions:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "job_application",
"type": "object",
"required": [
"notification_type",
"job_title",
"company_name",
"ats_provider"
],
"properties": {
"notification_type": {
"type": "string",
"enum": [
"application_received",
"screening_scheduled",
"interview_scheduled",
"interview_completed",
"offer_extended",
"offer_accepted",
"offer_declined",
"rejected",
"withdrawn"
]
},
"application_id": { "type": "string" },
"job_title": { "type": "string", "minLength": 1 },
"company_name": { "type": "string", "minLength": 1 },
"company_domain": { "type": "string", "format": "hostname" },
"ats_provider": {
"type": "string",
"enum": ["greenhouse", "lever", "workday", "ashby", "smartrecruiters", "icims", "unknown"]
},
"job_posting_id": { "type": "string" },
"job_location": { "type": "string" },
"recruiter_name": { "type": "string" },
"recruiter_email": { "type": "string", "format": "email" },
"hiring_manager_name":{ "type": "string" },
"event_date": { "type": "string", "format": "date-time" },
"event_location": { "type": "string" },
"event_location_url": { "type": "string", "format": "uri" },
"next_step": { "type": "string" },
"status_url": { "type": "string", "format": "uri" },
"date": { "type": "string", "format": "date" }
}
}
Parse via the API
POST the raw email (MIME or plain text) to /v1/parse with the schema you
want to extract against:
curl https://api.mailframe.ai/v1/parse \
-H "Authorization: Bearer ${MAILFRAME_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"schema_id": "job_application",
"raw_mime": "From: Acme Co Recruiting <no-reply@greenhouse.io>\r\nTo: jordan.rivera@example.com\r\nSubject: We received your application for Senior Backend Engineer\r\nDate: Thu, 03 Jul 2026 09:12:00 -0400\r\n\r\nHi Jordan,\r\n\r\nThanks for applying to the Senior Backend Engineer role at Acme Co. Our recruiting team has received your application and will review it shortly.\r\n\r\nPosition: Senior Backend Engineer\r\nLocation: Remote (US)\r\nRequisition: 4025001\r\nRecruiter: Priya Shah <priya.shah@acme.example>\r\n\r\nWe will be in touch about next steps. In the meantime, you can check the status of your application any time.\r\n\r\nView application:\r\nhttps://boards.greenhouse.io/acme/jobs/4025001/status"
}'
The call is synchronous: /v1/parse validates the extraction against
your schema and returns the typed JSON in the HTTP response, so you can
act on it inline — advance the application’s stage in your tracker, fire a
Slack ping on offer_extended, or open a candidate-experience follow-up on
rejected.
Routing on the result
The most useful split is by notification_type: it is the lane-decider.
An offer_extended deserves a different lane than a rejected — the first
is a high-signal event that should page the candidate (or, on the employer
side, the offer-approval workflow), while the second is an archival stage
change. interview_scheduled and screening_scheduled carry the
event_date, event_location, and event_location_url you need to drop the
interview onto a calendar. Use application_id (when present) as the
deduplication key — an ATS sends one message per stage transition, so a single
application produces a series of emails over its lifetime, and the same
application_id is the right join across all of them. When application_id
is absent, fall back to (company_name, job_posting_id, job_title).
Signed webhook delivery
Prefer asynchronous delivery? Signed webhook delivery — where MailFrame POSTs
the extraction result to your endpoint with an HMAC-SHA256 signature in the
MailFrame-Signature header and exponential-backoff retries — is available
during early access alongside the synchronous API. Inbox forwarding —
pointing a Gmail or Outlook filter at a unique inbox address MailFrame
assigns you — is on the roadmap.
Working with related email types? See the Parse Linear Notifications schema, the Parse GitHub Notifications schema, and the email-to-JSON API guide for a full walkthrough of POST /v1/parse. For a step-by-step tutorial that wires this schema into a real
/v1/parserequest, see Parse Job Application Emails to JSON.