Skip to content
calendar invites meetings google-calendar outlook icloud schema email-to-json tutorial

Parse Calendar Invite Emails to JSON

Turn Google Calendar, Outlook, and iCloud meeting invite emails into typed JSON: event title, organizer, attendees, start and end times, location, and response. Schema-first tutorial.

MailFrame Team

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_title is the meeting subject; organizer_email and organizer_name tell you who is running it. This is how you identify the core meeting details.
  • The when and where. start_datetime, end_datetime, timezone, and location. These are essential to ensure your system avoids double bookings.
  • The who. attendees is an array of everyone invited, including their name, email, required status, and current response_status.
  • The identity and state. event_uid is 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.

FieldTypeExampleNotes
event_titlestringQ3 roadmap reviewThe meeting subject
event_uidstring040000008200E000...Provider’s event UID when present
invitation_typeenuminvitationOne of invitation, update, cancellation
organizer_emailstringamelia.chen@example.comOrganizer’s address
organizer_namestringAmelia ChenOrganizer’s display name
attendeesarraysee belowOne entry per invitee
start_datetimestring2026-07-08T14:00:00-04:00Event start in ISO 8601
end_datetimestring2026-07-08T15:00:00-04:00Event end in ISO 8601
timezonestringAmerica/New_YorkIANA time zone when present
locationstringConference Room 4BPhysical location or conferencing link
descriptionstringReview the Q3 OKR draft...The invite body

Each entry in attendees carries:

FieldTypeExampleNotes
emailstringjordan.rivera@example.comInvitee address
namestringJordan RiveraInvitee display name
requiredbooleantruefalse for optional attendees
response_statusenumacceptedOne 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 location and 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.

Turn your inbox into an API

MailFrame parses raw email into typed JSON against your own schema — PDF and image input are planned. Request early developer access.

Request early access