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
| Field | Type | Example | Notes |
|---|---|---|---|
subscription_id | string | sub_1PqL2xK9 | The vendor’s subscription ID, as printed in the email |
product_name | string | Vercel Pro | The product or service name (not the plan) |
vendor_name | string | Vercel Inc. | The billing party — the company on the receipt |
vendor_domain | string | vercel.com | Sending or billing domain |
plan_name | string | Pro | The plan tier — Pro, Team, Business, Starter, etc. |
billing_interval | enum | monthly | One of monthly, quarterly, annual, weekly |
amount_cents | integer | 2000 | Amount charged in minor units, currency-agnostic |
currency | string | usd | ISO 4217, lower-cased |
renewal_date | string | 2026-08-14 | When the next charge will occur, ISO 8601 date |
next_renewal_date | string | 2026-08-14 | Alias of renewal_date — many vendors print the next renewal under either label, so both keys are populated when the email mentions one |
payment_method_brand | string | visa | Card brand, normalized — visa, mastercard, amex, discover, etc. |
payment_method_last4 | string | 4242 | Last 4 digits of the card on file |
status | enum | renewed | One of renewed, trial_ending, payment_failed, cancelled, changed |
manage_url | string | https://vercel.com/dashboard/billing/subscriptions/sub_1PqL2xK9 | ”Manage subscription” link when present |
invoice_url | string | https://vercel.com/dashboard/billing/invoices/inv_2026_07 | Link to the per-cycle invoice or receipt when present |
account_email | string | jordan.rivera@example.com | The recipient on the email — the account holder |
date | string | 2026-07-14 | Date 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. Apayment_failedstatus 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_dateandnext_renewal_dateare 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/parseas 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.