Google Calendar, Outlook, and iCloud send a fresh meeting-invite email the moment someone books time with you — and every one of them lays the event out differently. The fields a calendar sync, a CRM activity log, or a meeting-prep assistant needs are always the same: what’s the event, who’s running it, who’s attending, when (and in what time zone), and where.
If you are building an auto-responder, a calendar sync, a meeting room booking tool, or an attendance tracking workflow, the first thing you do with one of these invites is pull out that same handful of fields — only every app names and formats them differently, and half of them are buried in a multipart HTML body or an embedded iCalendar part (like Outlook’s BEGIN:VCALENDAR or Google’s text/calendar attachment). Writing a per-service regex for each is the kind of code that works right up until a provider ships a new template.
This post shows how to turn those invites into typed, validated JSON you can
route to a calendar backend, a meeting-prep workflow, or a customer-engagement timeline —
using a JSON Schema and a single POST to MailFrame’s /v1/parse endpoint. If
you want the ready-made schema reference, it lives at
Calendar Invites schema page; this
post is the why-and-how around it.
Everything below uses the shipped, synchronous path: you send raw MIME, you get schema-validated JSON back in the same HTTP response. No callback to wait on.
What you get
A calendar invite is one of the most common automated messages a person receives, and one of the most tedious to parse by hand. The fields worth extracting break down into a few groups:
- The event and organizer.
event_titleis the meeting subject;organizer_emailandorganizer_nametell you who is running it. This is how you identify the core meeting details. - The when and where.
start_datetime,end_datetime,timezone, andlocation. These are essential to ensure your system avoids double bookings. - The who.
attendeesis an array of everyone invited, including theirname,email,requiredstatus, and currentresponse_status. - The identity and state.
event_uidis the stable join key across updates;invitation_type(invitation,update,cancellation) is the lane-decider.
Parse those once, up front, and the rest of your pipeline — sync logic, notifications, or prep workflows — is plain data handling instead of string scraping.
| Field | Type | Example | Notes |
|---|---|---|---|
event_title | string | Q3 roadmap review | The meeting subject |
event_uid | string | 040000008200E000... | Provider’s event UID when present |
invitation_type | enum | invitation | One of invitation, update, cancellation |
organizer_email | string | amelia.chen@example.com | Organizer’s address |
organizer_name | string | Amelia Chen | Organizer’s display name |
attendees | array | see below | One entry per invitee |
start_datetime | string | 2026-07-08T14:00:00-04:00 | Event start in ISO 8601 |
end_datetime | string | 2026-07-08T15:00:00-04:00 | Event end in ISO 8601 |
timezone | string | America/New_York | IANA time zone when present |
location | string | Conference Room 4B | Physical location or conferencing link |
description | string | Review the Q3 OKR draft... | The invite body |
Each entry in attendees carries:
| Field | Type | Example | Notes |
|---|---|---|---|
email | string | jordan.rivera@example.com | Invitee address |
name | string | Jordan Rivera | Invitee display name |
required | boolean | true | false for optional attendees |
response_status | enum | accepted | One of accepted, declined, tentative, needs_action |
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 attendees if every event in your workflow
should carry at least one invitee, or restrict response_status to
{"accepted", "declined"} if you only care about confirmed outcomes:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "calendar_invite",
"type": "object",
"required": [
"event_title",
"invitation_type",
"start_datetime",
"end_datetime",
"organizer_email"
],
"properties": {
"event_title": { "type": "string", "minLength": 1 },
"event_uid": { "type": "string" },
"invitation_type": {
"type": "string",
"enum": ["invitation", "update", "cancellation"]
},
"organizer_email": { "type": "string", "format": "email" },
"organizer_name": { "type": "string" },
"attendees": {
"type": "array",
"items": {
"type": "object",
"required": ["email"],
"properties": {
"email": { "type": "string", "format": "email" },
"name": { "type": "string" },
"required": { "type": "boolean" },
"response_status": {
"type": "string",
"enum": ["accepted", "declined", "tentative", "needs_action"]
}
}
}
},
"start_datetime": { "type": "string", "format": "date-time" },
"end_datetime": { "type": "string", "format": "date-time" },
"timezone": { "type": "string" },
"location": { "type": "string" },
"location_url": { "type": "string", "format": "uri" },
"description": { "type": "string" },
"is_recurring": { "type": "boolean" },
"response_status": {
"type": "string",
"enum": ["accepted", "declined", "tentative", "needs_action"]
},
"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": "calendar_invite",
"input": { "type": "email", "raw": "<base64-encoded MIME>" }
}'
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 — push the event into your calendar backend, surface a
meeting-prep briefing, or trigger a follow-up workflow on cancellation.
Structured JSON output
The response wraps the extracted, schema-valid data in a data object:
{
"event_title": "Q3 roadmap review",
"event_uid": "040000008200E00074C5B7101A82E00800000000B0CAFED@example.com",
"invitation_type": "invitation",
"organizer_email": "amelia.chen@example.com",
"organizer_name": "Amelia Chen",
"attendees": [
{
"email": "amelia.chen@example.com",
"name": "Amelia Chen",
"required": true,
"response_status": "accepted"
},
{
"email": "jordan.rivera@example.com",
"name": "Jordan Rivera",
"required": true,
"response_status": "needs_action"
},
{
"email": "priya.shah@example.com",
"name": "Priya Shah",
"required": false,
"response_status": "needs_action"
}
],
"start_datetime": "2026-07-08T14:00:00-04:00",
"end_datetime": "2026-07-08T15:00:00-04:00",
"timezone": "America/New_York",
"location": "",
"location_url": "https://meet.google.com/abc-defg-hij",
"description": "Review the Q3 OKR draft before the 1:1 with the exec team.",
"is_recurring": false,
"response_status": "needs_action",
"date": "2026-07-07"
}
TypeScript type definition
Because the result is validated against your schema before it reaches you, your
application code can stay small. Check validation_errors first, then branch on
invitation_type (the broad lane).
interface ParseResult {
id: string;
status: "completed" | "failed";
validation_errors: unknown[];
data: CalendarInvite;
}
interface CalendarInvite {
event_title: string;
event_uid?: string;
invitation_type: "invitation" | "update" | "cancellation";
organizer_email: string;
organizer_name?: string;
attendees?: Array<{
email: string;
name?: string;
required?: boolean;
response_status?: "accepted" | "declined" | "tentative" | "needs_action";
}>;
start_datetime: string;
end_datetime: string;
timezone?: string;
location?: string;
location_url?: string;
description?: string;
is_recurring?: boolean;
response_status?: "accepted" | "declined" | "tentative" | "needs_action";
date?: string;
}
Python dataclass
If you’re using Python, you can define a TypedDict or dataclass to match your schema and gain robust type hints in your handler functions:
from typing import List, Optional, Literal
from typing_extensions import TypedDict
class Attendee(TypedDict, total=False):
email: str
name: Optional[str]
required: Optional[bool]
response_status: Optional[Literal["accepted", "declined", "tentative", "needs_action"]]
class CalendarInvite(TypedDict, total=False):
event_title: str
event_uid: Optional[str]
invitation_type: Literal["invitation", "update", "cancellation"]
organizer_email: str
organizer_name: Optional[str]
attendees: Optional[List[Attendee]]
start_datetime: str
end_datetime: str
timezone: Optional[str]
location: Optional[str]
location_url: Optional[str]
description: Optional[str]
is_recurring: Optional[bool]
response_status: Optional[Literal["accepted", "declined", "tentative", "needs_action"]]
date: Optional[str]
Signed webhook delivery
Signed async webhook delivery — where MailFrame POSTs a signed envelope
to your endpoint when a parse completes — is available during early
access alongside the synchronous API. Each delivery carries an
HMAC-SHA256 signature in the MailFrame-Signature header for
verification.
A webhook payload provides an envelope matching the synchronous response, looking like this:
{
"id": "evt_calendar_987",
"status": "completed",
"validation_errors": [],
"data": {
"event_title": "Q3 roadmap review",
"event_uid": "040000008200E00074C5B7101A82E00800000000B0CAFED@example.com",
"invitation_type": "invitation",
"organizer_email": "amelia.chen@example.com",
"organizer_name": "Amelia Chen",
"attendees": [
{
"email": "amelia.chen@example.com",
"name": "Amelia Chen",
"required": true,
"response_status": "accepted"
},
{
"email": "jordan.rivera@example.com",
"name": "Jordan Rivera",
"required": true,
"response_status": "needs_action"
},
{
"email": "priya.shah@example.com",
"name": "Priya Shah",
"required": false,
"response_status": "needs_action"
}
],
"start_datetime": "2026-07-08T14:00:00-04:00",
"end_datetime": "2026-07-08T15:00:00-04:00",
"timezone": "America/New_York",
"location": "",
"location_url": "https://meet.google.com/abc-defg-hij",
"description": "Review the Q3 OKR draft before the 1:1 with the exec team.",
"is_recurring": false,
"response_status": "needs_action",
"date": "2026-07-07"
}
}
Routing notes
The most useful split in your application is by invitation_type: any value of
invitation or update should refresh the local calendar entry keyed
on event_uid (carrying forward start_datetime, end_datetime,
location, and the per-attendee response_status). A
cancellation should soft-delete the local entry keyed on the same
event_uid rather than removing it from history completely.
Use event_uid as the primary deduplication key. Providers send one message per change, so a single
event in your calendar will produce several messages over its lifetime
(invitation, update, cancellation), and the same event_uid is the
right join across all of them. You can also route by organizer_email to associate events with specific contacts in your CRM or support platform.
What you can build
Because the data is structured and schema-validated up front, you can easily build robust integrations on top of these emails:
- Auto-responder: Automatically reply to or categorize meeting requests based on rules or the sender.
- Calendar sync: Keep a shadow copy of an external user’s availability and reflect updates correctly over time.
- Meeting room booking: Parse
locationand time fields to reserve physical rooms automatically on-site. - Attendance tracking: Track who accepted and declined important events over time to build reliable metrics and follow-up paths.
By trusting the schema, you eliminate the guesswork and constant regex updates needed to keep up with formatting drift in Microsoft, Google, or Apple’s email templates.