Every Apple App Store purchase — a one-time app, a subscription, an in-app purchase — generates a receipt email. The data you actually need (the order ID, the Apple ID, the itemized items, the tax breakdown, and whether the subscription auto-renews) is locked inside styled HTML that varies by region, purchase type, and Apple’s template version.
MailFrame turns those emails into a single typed JSON object you can write straight to your subscription management system, expense tracker, or customer dashboard.
The Apple App Store Receipts schema page covers the full field reference; this tutorial walks through parsing a real Apple receipt end to end, handling subscriptions, and wiring the result into your backend.
What you get
MailFrame’s apple_app_store_receipt schema extracts these fields from every
Apple App Store receipt email:
| Field | Type | Example | Notes |
|---|---|---|---|
order_id | string | ML8X9R2KTP | Apple order number |
document_no | string | MJDQ4LL/A | Internal document number when present |
apple_id | string | taylor@icloud.com | Apple ID (email) associated with the purchase |
items | array | see output | Each item: title, type, price_cents |
subtotal_cents | integer | 3999 | Pre-tax subtotal in minor units |
tax_cents | integer | 328 | Tax charged in minor units |
total_cents | integer | 4327 | Grand total in minor units |
currency | string | usd | ISO 4217, lower-cased |
date | string | 2026-05-21 | Purchase date normalized to ISO 8601 |
renewal | boolean | true | true if this is a subscription auto-renewal |
Sample input
A typical Apple App Store receipt email looks like this:
From: no_reply@email.apple.com
Subject: Your receipt from Apple.
Date: Thu, 21 May 2026 08:02:00 -0700
To: taylor@icloud.com
Dear Taylor,
Thank you for your purchase.
Apple ID: taylor@icloud.com
Order ID: ML8X9R2KTP
Document No.: MJDQ4LL/A
Billed to: Visa ending in 1234
May 21, 2026
Fantastical — Calendar & Tasks
Annual Subscription (auto-renewal) $39.99
Subtotal: $39.99
Tax: $3.28
Total: $43.27 USD
This subscription renews automatically. Manage subscriptions in Settings.
Structured JSON output
Pass the raw email and the schema to MailFrame. You can reference the pre-built
schema by id (schema_id) or pass an inline JSON Schema. Here it is with curl,
using the built-in apple_app_store_receipt schema against the receipt above:
curl -X POST https://api.mailframe.ai/v1/parse \
-H "Authorization: Bearer ${MAILFRAME_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"schema_id": "apple_app_store_receipt",
"raw_mime": "From: no_reply@email.apple.com\r\nTo: taylor@icloud.com\r\nSubject: Your receipt from Apple.\r\nDate: Thu, 21 May 2026 08:02:00 -0700\r\n\r\nDear Taylor,\r\n\r\nThank you for your purchase.\r\n\r\nApple ID: taylor@icloud.com\r\nOrder ID: ML8X9R2KTP\r\nDocument No.: MJDQ4LL/A\r\nBilled to: Visa ending in 1234\r\n\r\nMay 21, 2026\r\n\r\n Fantastical — Calendar & Tasks\r\n Annual Subscription (auto-renewal) $39.99\r\n\r\nSubtotal: $39.99\r\nTax: $3.28\r\nTotal: $43.27 USD\r\n\r\nThis subscription renews automatically. Manage subscriptions in Settings."
}'
The response wraps the extracted object in the standard MailFrame envelope:
{
"id": "parse_8f2a1c",
"status": "completed",
"validation_errors": [],
"data": {
"order_id": "ML8X9R2KTP",
"document_no": "MJDQ4LL/A",
"apple_id": "taylor@icloud.com",
"items": [
{
"title": "Fantastical — Calendar & Tasks Annual Subscription",
"type": "subscription",
"price_cents": 3999
}
],
"subtotal_cents": 3999,
"tax_cents": 328,
"total_cents": 4327,
"currency": "usd",
"date": "2026-05-21",
"renewal": true
}
}
TypeScript
const res = await fetch("https://api.mailframe.ai/v1/parse", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.MAILFRAME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
schema_id: "apple_app_store_receipt",
raw_mime: rawEmail,
}),
});
const result = await res.json();
// result.data.order_id → "ML8X9R2KTP"
// result.data.items[0].title → "Fantastical — Calendar & Tasks Annual Subscription"
// result.data.total_cents → 4327
// result.data.renewal → true
Python
import os
import requests
resp = requests.post(
"https://api.mailframe.ai/v1/parse",
headers={
"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}",
"Content-Type": "application/json",
},
json={
"schema_id": "apple_app_store_receipt",
"raw_mime": raw_email,
},
)
result = resp.json()
# result["data"]["order_id"] → "ML8X9R2KTP"
# result["data"]["items"][0]["title"] → "Fantastical — Calendar & Tasks Annual Subscription"
# result["data"]["total_cents"] → 4327
# result["data"]["renewal"] → True
Real-world patterns
Tracking subscription renewals
Apple sends a receipt email every time a subscription auto-renews. The
renewal boolean distinguishes initial purchases from renewals, so you can
route them to different workflows:
if (result.data.renewal) {
// Extend the subscription period — no new customer setup needed
await billing.extendSubscription({
appleId: result.data.apple_id,
orderId: result.data.order_id,
amount: result.data.total_cents,
currency: result.data.currency,
});
} else {
// First-time purchase — provision the subscription
await billing.activateSubscription({
appleId: result.data.apple_id,
orderId: result.data.order_id,
items: result.data.items,
amount: result.data.total_cents,
currency: result.data.currency,
});
}
Deduplication by order ID
Apple may send the same receipt multiple times — once immediately and again
if the customer requests a copy from their email. The order_id field is
your natural idempotency key:
// Upsert on order_id — second parse updates instead of duplicating
await db.query(
`INSERT INTO purchases (order_id, apple_id, total_cents, currency, renewal, purchase_date)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (order_id) DO UPDATE SET
renewal = EXCLUDED.renewal,
updated_at = NOW()`,
[
result.data.order_id,
result.data.apple_id,
result.data.total_cents,
result.data.currency,
result.data.renewal,
result.data.date,
],
);
Per-item expense tracking
Apple receipts often bundle multiple items — an app purchase plus a
subscription, or several in-app purchases in one transaction. The items
array captures each product independently so you can store and query
per-item data without re-parsing:
// Store each item as its own row for per-product queries
for (const item of result.data.items) {
await db.query(
`INSERT INTO purchase_items (order_id, title, type, price_cents)
VALUES ($1, $2, $3, $4)`,
[result.data.order_id, item.title, item.type, item.price_cents],
);
}
Reconciling subscription revenue
For businesses that sell through the App Store, each subscription renewal
email is a revenue event. By parsing every receipt against the
apple_app_store_receipt schema, you can build a subscription revenue
timeline without querying Apple’s App Store Connect API:
// Group renewals by month for revenue reporting
const renewals = await db.query(
`SELECT date_trunc('month', purchase_date) as month,
SUM(total_cents) as revenue_cents
FROM purchases
WHERE renewal = true
GROUP BY month
ORDER BY month DESC`,
);
Best practices
- Use
order_idas your idempotency key. Apple may send the same receipt multiple times. Upsert on the order ID to avoid duplicate rows. - Store
total_centsas an integer (minor units). Avoid floating-point rounding; convert to dollars only at the display layer. - Watch for the
renewalflag. A subscription renewal receipt has the same structure as an initial purchase but carriesrenewal: true. Your upsert logic should update the subscription period rather than inserting a new customer record. - Keep the raw MIME intact. Don’t pre-strip the HTML before sending — the sender domain and the receipt structure are signal the extractor leans on to tell Apple receipts from other payment processor emails.
- Test against all three purchase types. Apple sends different layouts for one-time app purchases, subscription purchases, and subscription renewals. Test against all three before going to production.
- Handle the
document_nofield. This internal Apple document number appears on some receipts but not others. It is optional in the schema, so your code should handle its absence gracefully.
Next steps
The example above POSTs from wherever you already hold the message — an IMAP poller on a shared purchasing inbox, an S3 trigger on a landing bucket, or a no-code pipeline forwarding Apple receipts to a single mailbox. That keeps ingestion in your own code and under your own retries, which is the model we generally recommend; the trade-offs are laid out in How to Choose an Email Parsing API.
For teams that would rather receive results asynchronously, signed webhook
delivery (HMAC-SHA256, with retries and delivery history) is available in early
access — see Designing Webhooks That Don’t Break
for how to verify and handle those payloads. Forwarding a purchasing inbox to a
unique MailFrame inbox address, instead of POSTing it yourself, is on the
roadmap; until it ships, the direct POST shown here is the supported path.
The full endpoint reference — request and response shapes, error handling, and language samples — lives in the API docs. If you want to try the Apple App Store receipt schema against your own receipts, request early access and we’ll help you wire it up.