Skip to content
apple app-store receipts subscriptions schema email-to-json tutorial

Parse Apple App Store Receipt Emails to JSON

Turn Apple App Store purchase and subscription receipt emails into typed JSON — order ID, Apple ID, purchased items, subscription type, tax, totals, and renewal status. Schema-first tutorial.

MailFrame Team

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:

FieldTypeExampleNotes
order_idstringML8X9R2KTPApple order number
document_nostringMJDQ4LL/AInternal document number when present
apple_idstringtaylor@icloud.comApple ID (email) associated with the purchase
itemsarraysee outputEach item: title, type, price_cents
subtotal_centsinteger3999Pre-tax subtotal in minor units
tax_centsinteger328Tax charged in minor units
total_centsinteger4327Grand total in minor units
currencystringusdISO 4217, lower-cased
datestring2026-05-21Purchase date normalized to ISO 8601
renewalbooleantruetrue 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_id as your idempotency key. Apple may send the same receipt multiple times. Upsert on the order ID to avoid duplicate rows.
  • Store total_cents as an integer (minor units). Avoid floating-point rounding; convert to dollars only at the display layer.
  • Watch for the renewal flag. A subscription renewal receipt has the same structure as an initial purchase but carries renewal: 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_no field. 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.

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