Skip to content
google-play android receipts subscriptions schema email-to-json tutorial

Parse Google Play Receipt Emails to JSON

Turn Google Play order receipt emails into typed JSON — order ID, item title, tax, totals, currency, and auto-renewal status. Schema-first tutorial.

MailFrame Team

Every Google Play transaction — a one-time app, a subscription, an in-app purchase — generates an order receipt email. The data you actually need (the order ID, the Google account email, the item title, the tax breakdown, and whether the subscription auto-renews) is locked inside styled HTML that varies by storefront, purchase type, and Google’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 Google Play Receipts schema page covers the full field reference; this tutorial walks through parsing a real Google Play receipt end to end, handling subscriptions, and wiring the result into your backend.

What you get

MailFrame’s google_play_receipt schema extracts these fields from every Google Play order receipt email:

FieldTypeExampleNotes
order_idstringGPA.1234-5678-9012-34567Google Play order ID
account_emailstringcasey@gmail.comGoogle account email on the order
item_titlestringYouTube Premium – Monthly subscriptionName of the purchased app, subscription, or item
item_typeenumsubscriptionOne of app, subscription, in_app
price_centsinteger1399Pre-tax item price in minor units
tax_centsinteger115Tax charged in minor units
total_centsinteger1514Total charged in minor units
currencystringusdISO 4217, lower-cased
datestring2026-05-21Purchase date normalized to ISO 8601
auto_renewingbooleantruetrue if the subscription will auto-renew

Sample input

A typical Google Play order receipt email looks like this:

From: googleplay-noreply@google.com
Subject: Your Google Play Order Receipt
Date: Thu, 21 May 2026 10:30:00 -0700
To: casey@gmail.com

Thank you for your purchase from Google Play!

Order number: GPA.1234-5678-9012-34567
Date: May 21, 2026

  YouTube Premium – Monthly subscription
  Charged to: Mastercard ending in 7890       $13.99

Tax:    $1.15
Total: $15.14 USD

This is an auto-renewing subscription.
Manage your subscriptions at play.google.com/subscriptions.

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 google_play_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": "google_play_receipt",
    "raw_mime": "From: googleplay-noreply@google.com\r\nTo: casey@gmail.com\r\nSubject: Your Google Play Order Receipt\r\nDate: Thu, 21 May 2026 10:30:00 -0700\r\n\r\nThank you for your purchase from Google Play!\r\n\r\nOrder number: GPA.1234-5678-9012-34567\r\nDate: May 21, 2026\r\n\r\n  YouTube Premium – Monthly subscription\r\n  Charged to: Mastercard ending in 7890       $13.99\r\n\r\nTax:    $1.15\r\nTotal: $15.14 USD\r\n\r\nThis is an auto-renewing subscription.\r\nManage your subscriptions at play.google.com/subscriptions."
  }'

The response wraps the extracted object in the standard MailFrame envelope:

{
  "id": "parse_b7c0d4",
  "status": "completed",
  "validation_errors": [],
  "data": {
    "order_id": "GPA.1234-5678-9012-34567",
    "account_email": "casey@gmail.com",
    "item_title": "YouTube Premium – Monthly subscription",
    "item_type": "subscription",
    "price_cents": 1399,
    "tax_cents": 115,
    "total_cents": 1514,
    "currency": "usd",
    "date": "2026-05-21",
    "auto_renewing": 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: "google_play_receipt",
    raw_mime: rawEmail,
  }),
});

const result = await res.json();
// result.data.order_id → "GPA.1234-5678-9012-34567"
// result.data.item_title → "YouTube Premium – Monthly subscription"
// result.data.total_cents → 1514
// result.data.auto_renewing → 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": "google_play_receipt",
        "raw_mime": raw_email,
    },
)
result = resp.json()
# result["data"]["order_id"] → "GPA.1234-5678-9012-34567"
# result["data"]["item_title"] → "YouTube Premium – Monthly subscription"
# result["data"]["total_cents"] → 1514
# result["data"]["auto_renewing"] → True

Real-world patterns

Tracking subscription purchases

Google Play sends an order receipt email for every transaction — an initial subscription purchase, each auto-renewal, and one-time purchases alike. The auto_renewing boolean tells you whether the subscription is set to renew, not whether this specific receipt is a renewal. To distinguish a new purchase from a renewal, check whether the order_id already exists in your database:

// Check if this order_id already exists — if so, it's a renewal
const existing = await db.query(
  `SELECT id FROM purchases WHERE order_id = $1`,
  [result.data.order_id],
);

if (existing.rows.length > 0) {
  // Renewal — update the subscription period
  await billing.extendSubscription({
    accountEmail: result.data.account_email,
    orderId: result.data.order_id,
    amount: result.data.total_cents,
    currency: result.data.currency,
  });
} else {
  // New purchase — record it
  await billing.recordPurchase({
    accountEmail: result.data.account_email,
    orderId: result.data.order_id,
    itemTitle: result.data.item_title,
    amount: result.data.total_cents,
    currency: result.data.currency,
  });
}

Deduplication by order ID

The order_id also serves as your idempotency key — separate from the new-vs-renewal check above, this handles the case where the same receipt arrives more than once (e.g., forwarded from multiple inboxes or re-ingested after a retry):

// Upsert on order_id — a second parse updates instead of duplicating
await db.query(
  `INSERT INTO purchases (order_id, account_email, item_title, total_cents, currency, auto_renewing, purchase_date)
   VALUES ($1, $2, $3, $4, $5, $6, $7)
   ON CONFLICT (order_id) DO UPDATE SET
     auto_renewing = EXCLUDED.auto_renewing,
     updated_at = NOW()`,
  [
    result.data.order_id,
    result.data.account_email,
    result.data.item_title,
    result.data.total_cents,
    result.data.currency,
    result.data.auto_renewing,
    result.data.date,
  ],
);

Categorize spend by purchase type

Google Play receipts identify each purchase by item_type — an app, a subscription, or an in_app item. (Each receipt covers one transaction, so the type applies to the single item.) Storing the type alongside the price lets you break spend down by category without re-parsing:

// Attribute spend to a category for expense reporting
await db.query(
  `INSERT INTO expenses (order_id, category, item_title, amount_cents, currency, spent_on)
   VALUES ($1, $2, $3, $4, $5, $6)`,
  [
    result.data.order_id,
    result.data.item_type,
    result.data.item_title,
    result.data.total_cents,
    result.data.currency,
    result.data.date,
  ],
);

Reconciling subscription revenue

For teams that sell through Google Play, each renewal receipt is a revenue event. By parsing every receipt against the google_play_receipt schema, you can build a subscription revenue timeline without querying the Google Play Developer API:

// Group auto-renewing subscription charges 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 auto_renewing = true
   GROUP BY month
   ORDER BY month DESC`,
);

Best practices

  • Use order_id as your idempotency key. Google Play may send the same receipt more than once. Upsert on the order ID to avoid duplicate rows.
  • Store total_cents as an integer (minor units). Avoid floating-point rounding; convert to the display currency only at the display layer.
  • Branch on item_type. Apps, subscriptions, and in-app items flow through different downstream logic. The enum lets you route each receipt without parsing the item title.
  • Watch the auto_renewing flag. A subscription that will renew has the same structure as one that won’t; only the flag differs. Use it to decide whether to schedule the next expected charge.
  • Keep the raw MIME intact. Don’t pre-strip the HTML before sending — the sender domain and receipt structure are signal the extractor leans on to tell Google Play receipts from other payment processor emails.
  • Test against all three purchase types. Google Play sends different layouts for one-time app purchases, subscription purchases, and in-app items. Test against all three before going to production.

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 Google Play 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. The direct POST shown here is the supported path for self-managed ingestion.

The full endpoint reference — request and response shapes, error handling, and language samples — lives in the API docs. If you also handle receipts from other stores, the Apple App Store and Stripe schemas follow the same envelope. Browse more walkthroughs on the blog, or if you want to try the Google Play 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