Skip to content
Tier 2 Schema

Parse Subscription Renewal Emails

Turn SaaS billing renewal emails from Vercel, Netlify, OpenAI, Notion, Linear, Figma, and others into typed JSON — subscription ID, product, plan, next renewal date, amount, payment method, and renewal status.

Vercel, Netlify, OpenAI, Notion, Linear, Figma, Datadog, and every other SaaS tool you pay for sends a renewal email the moment they charge your card for the next cycle — and every one of them lays the email out differently. Vercel writes “your Pro plan has been renewed”, Linear says “your subscription will renew on”, Notion puts the plan and amount in a two-column footer. The fields a finance-ops dashboard, an expense tool, or a “what’s renewing this month” digest needs are the same every time: which subscription, on which product, at what price, on which date, on which card, and whether the charge succeeded. MailFrame extracts those fields from the renewal email into typed, schema-validated JSON you can route straight into a renewals tracker, an expense ledger, or a payment-failure alerting workflow.

It works on the body of SaaS renewal confirmation and “your subscription will renew on” reminder emails — typically sent from billing@<vendor> or noreply@<vendor> addresses by the vendor’s billing system. Match on the message body and the renewal-specific wording (“renewed”, “will renew”, “subscription”, “next billing date”, “payment method”) rather than the From address alone. The subscription ID, product and plan names, billing interval, amount, currency, next renewal date, card brand and last four, and renewal status (renewed, trial ending, payment failed, cancelled, changed) all normalize into one consistent shape regardless of which vendor sent the message. PDF, image, and calendar-attachment input are on the roadmap; inbox forwarding is planned.

Fields MailFrame extracts

FieldTypeExampleNotes
subscription_idstringsub_1PqL2xK9The vendor’s subscription ID, as printed in the email
product_namestringVercel ProThe product or service name (not the plan)
vendor_namestringVercel Inc.The billing party — the company on the receipt
vendor_domainstringvercel.comSending or billing domain
plan_namestringProThe plan tier — Pro, Team, Business, Starter, etc.
billing_intervalenummonthlyOne of monthly, quarterly, annual, weekly
amount_centsinteger2000Amount charged in minor units, currency-agnostic
currencystringusdISO 4217, lower-cased
renewal_datestring2026-08-14When the next charge will occur, ISO 8601 date
next_renewal_datestring2026-08-14Alias of renewal_date — many vendors print the next renewal under either label, so both keys are populated when the email mentions one
payment_method_brandstringvisaCard brand, normalized — visa, mastercard, amex, discover, etc.
payment_method_last4string4242Last 4 digits of the card on file
statusenumrenewedOne of renewed, trial_ending, payment_failed, cancelled, changed
manage_urlstringhttps://vercel.com/dashboard/billing/subscriptions/sub_1PqL2xK9”Manage subscription” link when present
invoice_urlstringhttps://vercel.com/dashboard/billing/invoices/inv_2026_07Link to the per-cycle invoice or receipt when present
account_emailstringjordan.rivera@example.comThe recipient on the email — the account holder
datestring2026-07-14Date the renewal email was sent, ISO 8601

Sample input

A typical Vercel Pro plan renewal email looks like this:

From: billing@vercel.com
Subject: Your Vercel Pro plan has been renewed
Date: Tue, 14 Jul 2026 09:14:00 -0400
To: jordan.rivera@example.com

Hi Jordan,

Your Vercel Pro plan has been renewed. Your subscription will renew again on
Aug 14, 2026.

Subscription:   Vercel Pro
Plan:           Pro
Billing cycle:  Monthly
Amount:         $20.00 USD
Payment method: Visa ending in 4242
Subscription ID: sub_1PqL2xK9

Manage your subscription:
https://vercel.com/dashboard/billing/subscriptions/sub_1PqL2xK9

View your invoice:
https://vercel.com/dashboard/billing/invoices/inv_2026_07

The same shape is produced for Netlify, OpenAI, Notion, Linear, Figma, Datadog, and other SaaS renewal emails — status distinguishes a successful renewed confirmation from a trial_ending reminder, a payment_failed notice (alert on this!), a cancelled confirmation, or a changed notice when the plan or seat count has shifted. The amount and currency come from the line on the email that names the cycle charge, and subscription_id and next_renewal_date are the join keys for downstream dashboards.

Structured JSON output

{
  "subscription_id": "sub_1PqL2xK9",
  "product_name": "Vercel Pro",
  "vendor_name": "Vercel Inc.",
  "vendor_domain": "vercel.com",
  "plan_name": "Pro",
  "billing_interval": "monthly",
  "amount_cents": 2000,
  "currency": "usd",
  "renewal_date": "2026-08-14",
  "next_renewal_date": "2026-08-14",
  "payment_method_brand": "visa",
  "payment_method_last4": "4242",
  "status": "renewed",
  "manage_url": "https://vercel.com/dashboard/billing/subscriptions/sub_1PqL2xK9",
  "invoice_url": "https://vercel.com/dashboard/billing/invoices/inv_2026_07",
  "account_email": "jordan.rivera@example.com",
  "date": "2026-07-14"
}

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 payment_method_last4 if every renewal in your workflow must carry a card fingerprint, or restrict billing_interval to {"monthly", "annual"} if you only care about those two cycles:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "subscription_renewal",
  "type": "object",
  "required": [
    "subscription_id",
    "product_name",
    "vendor_name",
    "billing_interval",
    "amount_cents",
    "currency",
    "renewal_date",
    "status"
  ],
  "properties": {
    "subscription_id":       { "type": "string", "minLength": 1 },
    "product_name":          { "type": "string", "minLength": 1 },
    "vendor_name":           { "type": "string", "minLength": 1 },
    "vendor_domain":         { "type": "string", "format": "hostname" },
    "plan_name":             { "type": "string" },
    "billing_interval": {
      "type": "string",
      "enum": ["monthly", "quarterly", "annual", "weekly"]
    },
    "amount_cents":          { "type": "integer", "minimum": 0 },
    "currency":              { "type": "string", "minLength": 3, "maxLength": 3 },
    "renewal_date":          { "type": "string", "format": "date" },
    "next_renewal_date":     { "type": "string", "format": "date" },
    "payment_method_brand":  { "type": "string" },
    "payment_method_last4":  { "type": "string", "pattern": "^[0-9]{4}$" },
    "status": {
      "type": "string",
      "enum": ["renewed", "trial_ending", "payment_failed", "cancelled", "changed"]
    },
    "manage_url":            { "type": "string", "format": "uri" },
    "invoice_url":           { "type": "string", "format": "uri" },
    "account_email":         { "type": "string", "format": "email" },
    "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 $MAILF...KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schema_id": "subscription_renewal",
    "raw_mime": "From: billing@vercel.com\r\nTo: jordan.rivera@example.com\r\nSubject: Your Vercel Pro plan has been renewed\r\nDate: Tue, 14 Jul 2026 09:14:00 -0400\r\n\r\nHi Jordan,\r\n\r\nYour Vercel Pro plan has been renewed. Your subscription will renew again on Aug 14, 2026.\r\n\r\nSubscription:    Vercel Pro\r\nPlan:            Pro\r\nBilling cycle:   Monthly\r\nAmount:          $20.00 USD\r\nPayment method:  Visa ending in 4242\r\nSubscription ID: sub_1PqL2xK9\r\n\r\nManage your subscription:\r\nhttps://vercel.com/dashboard/billing/subscriptions/sub_1PqL2xK9\r\n\r\nView your invoice:\r\nhttps://vercel.com/dashboard/billing/invoices/inv_2026_07"
  }'

MailFrame returns the typed JSON in the same HTTP response. 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.

Operational notes

  • Dedup on subscription_id + renewal_date. A single subscription produces one email per cycle (renewal confirmation, optional reminder a few days ahead, optional receipt when the invoice closes). Grouping on (subscription_id, renewal_date) keeps a long-lived subscription from creating multiple rows per cycle in your renewals tracker.
  • Alert on payment_failed. A payment_failed status means the card on file was declined. Treat it as a high-priority routing signal — page finance-ops or open a support ticket — instead of archiving it alongside successful renewals.
  • renewal_date and next_renewal_date are intentionally duplicated. SaaS vendors phrase the next-charge date in different ways — some write “next billing date”, others “will renew on”, others “your next charge will occur on”. Both keys are populated from the same source value so downstream consumers can find the date regardless of which label they expect.
  • Body-only today. MailFrame reads the fields printed in the email body. PDF, image, and calendar-attachment input are on the roadmap; inbox forwarding — pointing your billing inbox at a unique address MailFrame assigns you — is planned as well. Until those ship, POST the raw email to /v1/parse as shown above.

Parsing other transactional mail? See the Stripe receipt schema for one-time payment confirmations, the invoice & payment-due schema for AP-side billing, browse the full schema library, or read the API docs and pricing for the free-tier limits.

Other schemas

Ship this schema in production

Define your fields once, then POST raw email to /v1/parse. MailFrame returns typed JSON in the HTTP response or via signed webhook delivery with retries, attempt history, dead-letter, and replay. (Inbox forwarding is on the roadmap.)

Request early access