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:
| Field | Type | Example | Notes |
|---|---|---|---|
order_id | string | GPA.1234-5678-9012-34567 | Google Play order ID |
account_email | string | casey@gmail.com | Google account email on the order |
item_title | string | YouTube Premium – Monthly subscription | Name of the purchased app, subscription, or item |
item_type | enum | subscription | One of app, subscription, in_app |
price_cents | integer | 1399 | Pre-tax item price in minor units |
tax_cents | integer | 115 | Tax charged in minor units |
total_cents | integer | 1514 | Total charged in minor units |
currency | string | usd | ISO 4217, lower-cased |
date | string | 2026-05-21 | Purchase date normalized to ISO 8601 |
auto_renewing | boolean | true | true 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_idas your idempotency key. Google Play may send the same receipt more than once. Upsert on the order ID to avoid duplicate rows. - Store
total_centsas 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_renewingflag. 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.