[{"data":1,"prerenderedAt":1785},["ShallowReactive",2],{"page-\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fstripe-metered-billing-configuration\u002F":3,"faq-schema-\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fstripe-metered-billing-configuration\u002F":1764},{"id":4,"title":5,"body":6,"description":1753,"extension":1754,"meta":1755,"navigation":322,"path":1760,"seo":1761,"stem":1762,"__hash__":1763},"content\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fstripe-metered-billing-configuration\u002Findex.md","Stripe Metered Billing Configuration",{"type":7,"value":8,"toc":1740},"minimark",[9,13,38,41,46,78,91,203,207,233,259,267,271,288,488,504,508,515,912,922,1015,1030,1034,1037,1303,1306,1405,1409,1434,1445,1449,1452,1525,1537,1541,1609,1613,1623,1627,1641,1647,1656,1665,1674,1678,1683,1710,1715,1736],[10,11,5],"h1",{"id":12},"stripe-metered-billing-configuration",[14,15,16,17,21,22,27,28,32,33,37],"p",{},"Usage-based pricing is only as trustworthy as the meter behind it, and on Stripe that meter is now the ",[18,19,20],"strong",{},"Billing Meters API"," — meters and meter events, not the deprecated subscription usage records you may have seen in older tutorials. Part of the ",[23,24,26],"a",{"href":25},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002F","Designing API Pricing Tiers"," guide, this page walks through creating a meter, choosing the right aggregation, reporting usage from a FastAPI app, reconciling reported events against the invoice, and hardening the whole loop against the failure modes that quietly leak revenue. It pairs with ",[23,29,31],{"href":30},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fintegrating-stripe-with-python-apis\u002F","integrating Stripe with Python APIs"," and ",[23,34,36],{"href":35},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fhow-to-charge-for-api-access-using-stripe\u002F","how to charge for API access using Stripe"," — read those first if you have not yet stood up the basic Stripe integration and checkout.",[14,39,40],{},"The commercial stakes here are direct. A metered bill is a number you show a customer once a month with your name attached to it. If that number is wrong high, you get disputes and churn; if it is wrong low, you eat the difference on every invoice for the life of the account. Getting the meter right is not a nice-to-have — it is the difference between a margin you can forecast and one you discover after the fact.",[42,43,45],"h2",{"id":44},"how-stripe-metered-billing-fits-together","How Stripe metered billing fits together",[14,47,48,49,52,53,57,58,61,62,65,66,69,70,73,74,77],{},"The current model has four moving parts. A ",[18,50,51],{},"Meter"," defines what you count and how — an ",[54,55,56],"code",{},"event_name"," like ",[54,59,60],{},"api_requests"," and an aggregation like ",[54,63,64],{},"sum",". A ",[18,67,68],{},"meter event"," is a single reported unit of usage, tied to a customer. A ",[18,71,72],{},"Price"," is configured as metered and bound to the meter. A ",[18,75,76],{},"Subscription"," to that price produces an invoice at period end, where Stripe rolls up the meter events into a billable quantity. Your job is to report meter events accurately as your API does work, then verify the rolled-up total against your own counter before trusting the invoice.",[14,79,80,81,85,86,90],{},"The flow is one-directional: usage event → meter aggregation → invoice line. You never write to the invoice; you write events, and Stripe aggregates. That separation is what makes the system idempotent-friendly, and it maps directly onto the ",[23,82,84],{"href":83},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fusage-based-vs-seat-based-pricing\u002F","usage-based pricing model"," you chose your billing axis around. It also means your own ",[23,87,89],{"href":88},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Ftracking-api-usage-and-analytics\u002Flogging-api-usage-events-to-postgres\u002F","usage event log in Postgres"," is not redundant with Stripe — it is the ground truth you reconcile the meter against.",[92,93,101,102,101,106,101,110,101,129,101,136,101,145,101,153,101,158,101,165,101,168,101,172,101,175,101,179,101,183,101,186,101,189,101,193,101,197,101,201],"svg",{"viewBox":94,"role":95,"ariaLabelledBy":96,"xmlns":99,"style":100},"0 0 720 200","img",[97,98],"smb-flow-t","smb-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[103,104,105],"title",{"id":97},"Stripe metered billing flow",[107,108,109],"desc",{"id":98},"A usage event from the API is reported as a meter event, Stripe aggregates events into a meter total, and the subscription produces an invoice line.",[111,112,113,114,101],"defs",{},"\n    ",[115,116,123,124,113],"marker",{"id":117,"viewBox":118,"refX":119,"refY":120,"markerWidth":121,"markerHeight":121,"orient":122},"smb-flow-arrow","0 0 10 10","9","5","7","auto-start-reverse","\n      ",[125,126],"path",{"d":127,"fill":128},"M0 0 L10 5 L0 10 z","var(--c-text-muted)",[130,131],"rect",{"x":132,"y":132,"width":133,"height":134,"fill":135},"0","720","200","var(--c-surface)",[130,137],{"x":138,"y":139,"width":140,"height":141,"rx":142,"fill":135,"stroke":143,"style":144},"15","70","160","60","10","var(--c-blue)","stroke-width:2;",[146,147,152],"text",{"x":148,"y":149,"fill":150,"style":151},"95","98","var(--c-text)","text-anchor:middle;font-size:13;font-family:var(--font-sans);","API request",[146,154,157],{"x":148,"y":155,"fill":128,"style":156},"116","text-anchor:middle;font-size:11;font-family:var(--font-sans);","does work",[159,160],"line",{"x1":161,"y1":162,"x2":163,"y2":162,"stroke":128,"style":164},"175","100","230","stroke-width:2;marker-end:url(#smb-flow-arrow);",[130,166],{"x":163,"y":139,"width":140,"height":141,"rx":142,"fill":135,"stroke":167,"style":144},"var(--c-teal)",[146,169,171],{"x":170,"y":149,"fill":150,"style":151},"310","Meter event",[146,173,174],{"x":170,"y":155,"fill":128,"style":156},"v1\u002Fbilling",[159,176],{"x1":177,"y1":162,"x2":178,"y2":162,"stroke":128,"style":164},"390","445",[130,180],{"x":178,"y":139,"width":181,"height":141,"rx":142,"fill":135,"stroke":182,"style":144},"120","var(--c-yellow)",[146,184,51],{"x":185,"y":149,"fill":150,"style":151},"505",[146,187,188],{"x":185,"y":155,"fill":128,"style":156},"sum\u002Fperiod",[159,190],{"x1":191,"y1":162,"x2":192,"y2":162,"stroke":128,"style":164},"565","620",[130,194],{"x":192,"y":139,"width":195,"height":141,"rx":142,"fill":135,"stroke":196,"style":144},"90","var(--c-coral)",[146,198,200],{"x":199,"y":149,"fill":150,"style":151},"665","Invoice",[146,202,159],{"x":199,"y":155,"fill":128,"style":156},[42,204,206],{"id":205},"choosing-an-aggregation-before-you-write-code","Choosing an aggregation before you write code",[14,208,209,210,214,215,217,218,221,222,225,226,228,229,232],{},"The aggregation you pick on the meter is not cosmetic — it changes what a meter event ",[211,212,213],"em",{},"means",", and you cannot swap it later without creating a new meter and migrating subscriptions. Stripe gives you three practical formulas. ",[54,216,64],{}," adds the ",[54,219,220],{},"value"," of every event, which is what you want when each request can consume a variable amount (tokens processed, megabytes rendered, rows exported). ",[54,223,224],{},"count"," ignores ",[54,227,220],{}," and tallies events, which fits a flat per-call charge. ",[54,230,231],{},"last"," takes the most recent value in the window, which suits gauge-style metrics like \"seats currently active\" or \"GB currently stored\".",[14,234,235,236,238,239,241,242,246,247,249,250,252,253,255,256,258],{},"Pick the one that matches your billing axis exactly. If you charge per API call, use ",[54,237,224],{}," and stop sending a ",[54,240,220],{}," you do not need. If you charge per token — the common shape for anything wrapping an LLM, which the guide on ",[23,243,245],{"href":244},"\u002Fautomating-side-hustle-operations-with-apis\u002Fautomating-ai-workflows-with-python-apis\u002Fcontrolling-llm-api-costs-in-production\u002F","controlling LLM API costs in production"," digs into — use ",[54,248,64],{}," and report the token count as ",[54,251,220],{},". Getting this wrong is expensive: a meter set to ",[54,254,224],{}," when you meant ",[54,257,64],{}," bills every 4,000-token request as a single unit, and you will not notice until margin analysis shows revenue that cannot cover compute.",[14,260,261,262,266],{},"The metered Price you bind to the meter is a separate decision. A flat per-unit price is simplest, but Stripe also supports graduated and volume tiers — the first 100k units at one rate, the next 900k cheaper. Model that curve against your real ",[23,263,265],{"href":264},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fcalculating-cost-per-api-request\u002F","cost per API request"," so every tier stays above your marginal cost, not just the first one.",[42,268,270],{"id":269},"step-1-create-the-meter","Step 1: Create the meter",[14,272,273,274,276,277,280,281,284,285,287],{},"You create the meter once, in the dashboard or via the API. The ",[54,275,56],{}," is the contract between your code and Stripe — every meter event you send must carry it. The ",[54,278,279],{},"customer_mapping"," tells Stripe which payload field identifies the customer, and ",[54,282,283],{},"value_settings"," names the payload field Stripe reads for ",[54,286,64],{}," aggregation.",[289,290,295],"pre",{"className":291,"code":292,"language":293,"meta":294,"style":294},"language-python shiki shiki-themes github-light github-dark","import os\nimport stripe\n\nstripe.api_key = os.getenv(\"STRIPE_SECRET_KEY\")\n\nmeter = stripe.billing.Meter.create(\n    display_name=\"API Requests\",\n    event_name=os.getenv(\"METER_EVENT_NAME\", \"api_requests\"),\n    default_aggregation={\"formula\": \"sum\"},\n    customer_mapping={\"event_payload_key\": \"stripe_customer_id\", \"type\": \"by_id\"},\n    value_settings={\"event_payload_key\": \"value\"},\n)\nprint(meter.id)  # store this; bind a metered Price to it in the dashboard\n","python","",[54,296,297,309,317,324,343,348,359,374,397,420,450,469,474],{"__ignoreMap":294},[298,299,301,305],"span",{"class":159,"line":300},1,[298,302,304],{"class":303},"szBVR","import",[298,306,308],{"class":307},"sVt8B"," os\n",[298,310,312,314],{"class":159,"line":311},2,[298,313,304],{"class":303},[298,315,316],{"class":307}," stripe\n",[298,318,320],{"class":159,"line":319},3,[298,321,323],{"emptyLinePlaceholder":322},true,"\n",[298,325,327,330,333,336,340],{"class":159,"line":326},4,[298,328,329],{"class":307},"stripe.api_key ",[298,331,332],{"class":303},"=",[298,334,335],{"class":307}," os.getenv(",[298,337,339],{"class":338},"sZZnC","\"STRIPE_SECRET_KEY\"",[298,341,342],{"class":307},")\n",[298,344,346],{"class":159,"line":345},5,[298,347,323],{"emptyLinePlaceholder":322},[298,349,351,354,356],{"class":159,"line":350},6,[298,352,353],{"class":307},"meter ",[298,355,332],{"class":303},[298,357,358],{"class":307}," stripe.billing.Meter.create(\n",[298,360,362,366,368,371],{"class":159,"line":361},7,[298,363,365],{"class":364},"s4XuR","    display_name",[298,367,332],{"class":303},[298,369,370],{"class":338},"\"API Requests\"",[298,372,373],{"class":307},",\n",[298,375,377,380,382,385,388,391,394],{"class":159,"line":376},8,[298,378,379],{"class":364},"    event_name",[298,381,332],{"class":303},[298,383,384],{"class":307},"os.getenv(",[298,386,387],{"class":338},"\"METER_EVENT_NAME\"",[298,389,390],{"class":307},", ",[298,392,393],{"class":338},"\"api_requests\"",[298,395,396],{"class":307},"),\n",[298,398,400,403,405,408,411,414,417],{"class":159,"line":399},9,[298,401,402],{"class":364},"    default_aggregation",[298,404,332],{"class":303},[298,406,407],{"class":307},"{",[298,409,410],{"class":338},"\"formula\"",[298,412,413],{"class":307},": ",[298,415,416],{"class":338},"\"sum\"",[298,418,419],{"class":307},"},\n",[298,421,423,426,428,430,433,435,438,440,443,445,448],{"class":159,"line":422},10,[298,424,425],{"class":364},"    customer_mapping",[298,427,332],{"class":303},[298,429,407],{"class":307},[298,431,432],{"class":338},"\"event_payload_key\"",[298,434,413],{"class":307},[298,436,437],{"class":338},"\"stripe_customer_id\"",[298,439,390],{"class":307},[298,441,442],{"class":338},"\"type\"",[298,444,413],{"class":307},[298,446,447],{"class":338},"\"by_id\"",[298,449,419],{"class":307},[298,451,453,456,458,460,462,464,467],{"class":159,"line":452},11,[298,454,455],{"class":364},"    value_settings",[298,457,332],{"class":303},[298,459,407],{"class":307},[298,461,432],{"class":338},[298,463,413],{"class":307},[298,465,466],{"class":338},"\"value\"",[298,468,419],{"class":307},[298,470,472],{"class":159,"line":471},12,[298,473,342],{"class":307},[298,475,477,481,484],{"class":159,"line":476},13,[298,478,480],{"class":479},"sj4cs","print",[298,482,483],{"class":307},"(meter.id)  ",[298,485,487],{"class":486},"sJ8bj","# store this; bind a metered Price to it in the dashboard\n",[14,489,490,491,494,495,498,499,503],{},"Then create a ",[18,492,493],{},"metered Price"," bound to this meter (dashboard or API), and subscribe customers to it. Store the resulting ",[54,496,497],{},"stripe_customer_id"," on each of your accounts so you can address meter events to the right customer. Do this in a test-mode workspace first and drive a full billing period with ",[23,500,502],{"href":501},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fintegrating-stripe-with-python-apis\u002Ftesting-stripe-integrations-with-test-clocks\u002F","Stripe test clocks"," before a single real event flows — a meter you misconfigured is far cheaper to fix before it has issued an invoice.",[42,505,507],{"id":506},"step-2-report-usage-from-fastapi","Step 2: Report usage from FastAPI",[14,509,510,511,514],{},"Report a meter event after the work succeeds, and do it off the request's critical path. A background task or a queue keeps billing from adding a Stripe round trip to every response. Pass an ",[54,512,513],{},"identifier"," for idempotency so a retried report is counted once.",[289,516,518],{"className":291,"code":517,"language":293,"meta":294,"style":294},"import os\nimport stripe\nfrom fastapi import FastAPI, BackgroundTasks, Depends, Header\n\napp = FastAPI()\nstripe.api_key = os.getenv(\"STRIPE_SECRET_KEY\")\nEVENT_NAME = os.getenv(\"METER_EVENT_NAME\", \"api_requests\")\n\ndef report_meter_event(stripe_customer_id: str, units: int, identifier: str) -> None:\n    \"\"\"Report usage to Stripe. identifier makes the report idempotent on retries.\"\"\"\n    try:\n        stripe.billing.MeterEvent.create(\n            event_name=EVENT_NAME,\n            identifier=identifier,  # dedupes replays within Stripe's window\n            payload={\"stripe_customer_id\": stripe_customer_id, \"value\": str(units)},\n        )\n    except stripe.StripeError as exc:\n        # Never crash the request path on a billing hiccup; log and let reconciliation catch it.\n        print(f\"meter report failed for {stripe_customer_id}: {exc}\")\n\nasync def current_customer(x_api_key: str = Header(...)) -> str:\n    # Replace with a real lookup mapping the API key to its Stripe customer id.\n    return os.getenv(\"DEMO_STRIPE_CUSTOMER_ID\", \"cus_demo\")\n\n@app.post(\"\u002Fv1\u002Fprocess\")\nasync def process(bg: BackgroundTasks, request_id: str, customer: str = Depends(current_customer)):\n    result = {\"processed\": True}            # ... do the real work ...\n    bg.add_task(report_meter_event, customer, 1, identifier=request_id)\n    return result\n",[54,519,520,526,532,545,549,559,571,589,593,628,633,640,645,656,670,694,700,715,721,757,762,794,800,818,823,836,861,886,904],{"__ignoreMap":294},[298,521,522,524],{"class":159,"line":300},[298,523,304],{"class":303},[298,525,308],{"class":307},[298,527,528,530],{"class":159,"line":311},[298,529,304],{"class":303},[298,531,316],{"class":307},[298,533,534,537,540,542],{"class":159,"line":319},[298,535,536],{"class":303},"from",[298,538,539],{"class":307}," fastapi ",[298,541,304],{"class":303},[298,543,544],{"class":307}," FastAPI, BackgroundTasks, Depends, Header\n",[298,546,547],{"class":159,"line":326},[298,548,323],{"emptyLinePlaceholder":322},[298,550,551,554,556],{"class":159,"line":345},[298,552,553],{"class":307},"app ",[298,555,332],{"class":303},[298,557,558],{"class":307}," FastAPI()\n",[298,560,561,563,565,567,569],{"class":159,"line":350},[298,562,329],{"class":307},[298,564,332],{"class":303},[298,566,335],{"class":307},[298,568,339],{"class":338},[298,570,342],{"class":307},[298,572,573,576,579,581,583,585,587],{"class":159,"line":361},[298,574,575],{"class":479},"EVENT_NAME",[298,577,578],{"class":303}," =",[298,580,335],{"class":307},[298,582,387],{"class":338},[298,584,390],{"class":307},[298,586,393],{"class":338},[298,588,342],{"class":307},[298,590,591],{"class":159,"line":376},[298,592,323],{"emptyLinePlaceholder":322},[298,594,595,598,602,605,608,611,614,617,619,622,625],{"class":159,"line":399},[298,596,597],{"class":303},"def",[298,599,601],{"class":600},"sScJk"," report_meter_event",[298,603,604],{"class":307},"(stripe_customer_id: ",[298,606,607],{"class":479},"str",[298,609,610],{"class":307},", units: ",[298,612,613],{"class":479},"int",[298,615,616],{"class":307},", identifier: ",[298,618,607],{"class":479},[298,620,621],{"class":307},") -> ",[298,623,624],{"class":479},"None",[298,626,627],{"class":307},":\n",[298,629,630],{"class":159,"line":422},[298,631,632],{"class":338},"    \"\"\"Report usage to Stripe. identifier makes the report idempotent on retries.\"\"\"\n",[298,634,635,638],{"class":159,"line":452},[298,636,637],{"class":303},"    try",[298,639,627],{"class":307},[298,641,642],{"class":159,"line":471},[298,643,644],{"class":307},"        stripe.billing.MeterEvent.create(\n",[298,646,647,650,652,654],{"class":159,"line":476},[298,648,649],{"class":364},"            event_name",[298,651,332],{"class":303},[298,653,575],{"class":479},[298,655,373],{"class":307},[298,657,659,662,664,667],{"class":159,"line":658},14,[298,660,661],{"class":364},"            identifier",[298,663,332],{"class":303},[298,665,666],{"class":307},"identifier,  ",[298,668,669],{"class":486},"# dedupes replays within Stripe's window\n",[298,671,673,676,678,680,682,685,687,689,691],{"class":159,"line":672},15,[298,674,675],{"class":364},"            payload",[298,677,332],{"class":303},[298,679,407],{"class":307},[298,681,437],{"class":338},[298,683,684],{"class":307},": stripe_customer_id, ",[298,686,466],{"class":338},[298,688,413],{"class":307},[298,690,607],{"class":479},[298,692,693],{"class":307},"(units)},\n",[298,695,697],{"class":159,"line":696},16,[298,698,699],{"class":307},"        )\n",[298,701,703,706,709,712],{"class":159,"line":702},17,[298,704,705],{"class":303},"    except",[298,707,708],{"class":307}," stripe.StripeError ",[298,710,711],{"class":303},"as",[298,713,714],{"class":307}," exc:\n",[298,716,718],{"class":159,"line":717},18,[298,719,720],{"class":486},"        # Never crash the request path on a billing hiccup; log and let reconciliation catch it.\n",[298,722,724,727,730,733,736,738,740,743,745,747,750,752,755],{"class":159,"line":723},19,[298,725,726],{"class":479},"        print",[298,728,729],{"class":307},"(",[298,731,732],{"class":303},"f",[298,734,735],{"class":338},"\"meter report failed for ",[298,737,407],{"class":479},[298,739,497],{"class":307},[298,741,742],{"class":479},"}",[298,744,413],{"class":338},[298,746,407],{"class":479},[298,748,749],{"class":307},"exc",[298,751,742],{"class":479},[298,753,754],{"class":338},"\"",[298,756,342],{"class":307},[298,758,760],{"class":159,"line":759},20,[298,761,323],{"emptyLinePlaceholder":322},[298,763,765,768,771,774,777,779,781,784,787,790,792],{"class":159,"line":764},21,[298,766,767],{"class":303},"async",[298,769,770],{"class":303}," def",[298,772,773],{"class":600}," current_customer",[298,775,776],{"class":307},"(x_api_key: ",[298,778,607],{"class":479},[298,780,578],{"class":303},[298,782,783],{"class":307}," Header(",[298,785,786],{"class":479},"...",[298,788,789],{"class":307},")) -> ",[298,791,607],{"class":479},[298,793,627],{"class":307},[298,795,797],{"class":159,"line":796},22,[298,798,799],{"class":486},"    # Replace with a real lookup mapping the API key to its Stripe customer id.\n",[298,801,803,806,808,811,813,816],{"class":159,"line":802},23,[298,804,805],{"class":303},"    return",[298,807,335],{"class":307},[298,809,810],{"class":338},"\"DEMO_STRIPE_CUSTOMER_ID\"",[298,812,390],{"class":307},[298,814,815],{"class":338},"\"cus_demo\"",[298,817,342],{"class":307},[298,819,821],{"class":159,"line":820},24,[298,822,323],{"emptyLinePlaceholder":322},[298,824,826,829,831,834],{"class":159,"line":825},25,[298,827,828],{"class":600},"@app.post",[298,830,729],{"class":307},[298,832,833],{"class":338},"\"\u002Fv1\u002Fprocess\"",[298,835,342],{"class":307},[298,837,839,841,843,846,849,851,854,856,858],{"class":159,"line":838},26,[298,840,767],{"class":303},[298,842,770],{"class":303},[298,844,845],{"class":600}," process",[298,847,848],{"class":307},"(bg: BackgroundTasks, request_id: ",[298,850,607],{"class":479},[298,852,853],{"class":307},", customer: ",[298,855,607],{"class":479},[298,857,578],{"class":303},[298,859,860],{"class":307}," Depends(current_customer)):\n",[298,862,864,867,869,872,875,877,880,883],{"class":159,"line":863},27,[298,865,866],{"class":307},"    result ",[298,868,332],{"class":303},[298,870,871],{"class":307}," {",[298,873,874],{"class":338},"\"processed\"",[298,876,413],{"class":307},[298,878,879],{"class":479},"True",[298,881,882],{"class":307},"}            ",[298,884,885],{"class":486},"# ... do the real work ...\n",[298,887,889,892,895,897,899,901],{"class":159,"line":888},28,[298,890,891],{"class":307},"    bg.add_task(report_meter_event, customer, ",[298,893,894],{"class":479},"1",[298,896,390],{"class":307},[298,898,513],{"class":364},[298,900,332],{"class":303},[298,902,903],{"class":307},"request_id)\n",[298,905,907,909],{"class":159,"line":906},29,[298,908,805],{"class":303},[298,910,911],{"class":307}," result\n",[14,913,914,915,917,918,921],{},"The ",[54,916,513],{}," is the linchpin. Use a value that is unique per billable unit of work — your own request id, not a timestamp — so Stripe deduplicates replays and your bill stays correct under retries. Keep a local copy of every reported ",[54,919,920],{},"(customer, units, identifier)"," so you can reconcile later. The diagram below shows why the ordering matters: the customer's response returns as soon as the work is done, and the meter report happens after, on a lane that can fail without the caller ever noticing.",[92,923,101,928,101,931,101,934,101,941,101,944,101,950,101,955,101,958,101,962,101,965,101,969,101,974,101,976,101,978,101,981,101,987,101,993,101,997,101,999,101,1003,101,1007,101,1011],{"viewBox":924,"role":95,"ariaLabelledBy":925,"xmlns":99,"style":100},"0 0 720 300",[926,927],"smb-seq-t","smb-seq-d",[103,929,930],{"id":926},"Billing report kept off the request critical path",[107,932,933],{"id":927},"The client receives its response as soon as the API finishes work, then a background task reports the meter event to Stripe on a separate lane.",[111,935,113,936,101],{},[115,937,123,939,113],{"id":938,"viewBox":118,"refX":119,"refY":120,"markerWidth":121,"markerHeight":121,"orient":122},"smb-seq-arrow",[125,940],{"d":127,"fill":128},[130,942],{"x":132,"y":132,"width":133,"height":943,"fill":135},"300",[130,945],{"x":946,"y":947,"width":948,"height":946,"rx":949,"fill":135,"stroke":143,"style":144},"40","20","150","8",[146,951,954],{"x":952,"y":953,"fill":150,"style":151},"115","45","Client",[130,956],{"x":957,"y":947,"width":948,"height":946,"rx":949,"fill":135,"stroke":167,"style":144},"285",[146,959,961],{"x":960,"y":953,"fill":150,"style":151},"360","Your FastAPI app",[130,963],{"x":964,"y":947,"width":948,"height":946,"rx":949,"fill":135,"stroke":196,"style":144},"530",[146,966,968],{"x":967,"y":953,"fill":150,"style":151},"605","Stripe meter",[159,970],{"x1":952,"y1":141,"x2":952,"y2":971,"stroke":972,"style":973},"280","var(--c-border)","stroke-width:1;stroke-dasharray:4 4;",[159,975],{"x1":960,"y1":141,"x2":960,"y2":971,"stroke":972,"style":973},[159,977],{"x1":967,"y1":141,"x2":967,"y2":971,"stroke":972,"style":973},[159,979],{"x1":952,"y1":162,"x2":960,"y2":162,"stroke":128,"style":980},"stroke-width:2;marker-end:url(#smb-seq-arrow);",[146,982,986],{"x":983,"y":984,"fill":150,"style":985},"237","92","text-anchor:middle;font-size:12;font-family:var(--font-sans);","POST \u002Fv1\u002Fprocess",[130,988],{"x":989,"y":181,"width":947,"height":946,"rx":990,"fill":991,"stroke":167,"style":992},"350","3","var(--c-surface-alt)","stroke-width:1.5;",[146,994,996],{"x":960,"y":995,"fill":128,"style":156},"178","do work",[159,998],{"x1":960,"y1":134,"x2":952,"y2":134,"stroke":128,"style":980},[146,1000,1002],{"x":983,"y":1001,"fill":150,"style":985},"192","200 OK (fast path)",[159,1004],{"x1":960,"y1":1005,"x2":967,"y2":1005,"stroke":196,"style":1006},"245","stroke-width:2;stroke-dasharray:6 4;marker-end:url(#smb-seq-arrow);",[146,1008,1010],{"x":1009,"y":983,"fill":150,"style":985},"482","MeterEvent.create",[146,1012,1014],{"x":1009,"y":1013,"fill":128,"style":156},"266","background task, may retry",[14,1016,1017,1018,1021,1022,1024,1025,1029],{},"For anything above a few hundred requests per second, a per-request ",[54,1019,1020],{},"BackgroundTasks"," call is the wrong tool — you want a queue. Push ",[54,1023,920],{}," onto Redis or a ",[23,1026,1028],{"href":1027},"\u002Fscaling-and-operating-production-python-apis\u002Frunning-background-jobs-with-celery\u002F","Celery-style background job"," and drain it with a worker that batches events. That also survives a process restart, which an in-process background task does not.",[42,1031,1033],{"id":1032},"step-3-reconcile-against-the-invoice","Step 3: Reconcile against the invoice",[14,1035,1036],{},"Trust, then verify. Before a period's invoice finalizes, read Stripe's aggregated total for the customer and compare it to your local counter. A drift means dropped events (report failures) or duplicates (a broken identifier) — both are revenue bugs, and both are invisible until you look for them.",[289,1038,1040],{"className":291,"code":1039,"language":293,"meta":294,"style":294},"import os\nimport stripe\n\nstripe.api_key = os.getenv(\"STRIPE_SECRET_KEY\")\n\ndef stripe_meter_total(meter_id: str, customer_id: str, start: int, end: int) -> int:\n    \"\"\"Pull Stripe's aggregated usage for a customer over a window.\"\"\"\n    summaries = stripe.billing.Meter.list_event_summaries(\n        meter_id,\n        customer=customer_id,\n        start_time=start,\n        end_time=end,\n    )\n    return int(sum(float(s[\"aggregated_value\"]) for s in summaries.auto_paging_iter()))\n\ndef reconcile(meter_id: str, customer_id: str, start: int, end: int, local_count: int) -> dict:\n    reported = stripe_meter_total(meter_id, customer_id, start, end)\n    drift = reported - local_count\n    return {\"stripe\": reported, \"local\": local_count, \"drift\": drift, \"ok\": drift == 0}\n",[54,1041,1042,1048,1054,1058,1070,1074,1107,1112,1122,1127,1137,1147,1157,1162,1199,1203,1238,1248,1264],{"__ignoreMap":294},[298,1043,1044,1046],{"class":159,"line":300},[298,1045,304],{"class":303},[298,1047,308],{"class":307},[298,1049,1050,1052],{"class":159,"line":311},[298,1051,304],{"class":303},[298,1053,316],{"class":307},[298,1055,1056],{"class":159,"line":319},[298,1057,323],{"emptyLinePlaceholder":322},[298,1059,1060,1062,1064,1066,1068],{"class":159,"line":326},[298,1061,329],{"class":307},[298,1063,332],{"class":303},[298,1065,335],{"class":307},[298,1067,339],{"class":338},[298,1069,342],{"class":307},[298,1071,1072],{"class":159,"line":345},[298,1073,323],{"emptyLinePlaceholder":322},[298,1075,1076,1078,1081,1084,1086,1089,1091,1094,1096,1099,1101,1103,1105],{"class":159,"line":350},[298,1077,597],{"class":303},[298,1079,1080],{"class":600}," stripe_meter_total",[298,1082,1083],{"class":307},"(meter_id: ",[298,1085,607],{"class":479},[298,1087,1088],{"class":307},", customer_id: ",[298,1090,607],{"class":479},[298,1092,1093],{"class":307},", start: ",[298,1095,613],{"class":479},[298,1097,1098],{"class":307},", end: ",[298,1100,613],{"class":479},[298,1102,621],{"class":307},[298,1104,613],{"class":479},[298,1106,627],{"class":307},[298,1108,1109],{"class":159,"line":361},[298,1110,1111],{"class":338},"    \"\"\"Pull Stripe's aggregated usage for a customer over a window.\"\"\"\n",[298,1113,1114,1117,1119],{"class":159,"line":376},[298,1115,1116],{"class":307},"    summaries ",[298,1118,332],{"class":303},[298,1120,1121],{"class":307}," stripe.billing.Meter.list_event_summaries(\n",[298,1123,1124],{"class":159,"line":399},[298,1125,1126],{"class":307},"        meter_id,\n",[298,1128,1129,1132,1134],{"class":159,"line":422},[298,1130,1131],{"class":364},"        customer",[298,1133,332],{"class":303},[298,1135,1136],{"class":307},"customer_id,\n",[298,1138,1139,1142,1144],{"class":159,"line":452},[298,1140,1141],{"class":364},"        start_time",[298,1143,332],{"class":303},[298,1145,1146],{"class":307},"start,\n",[298,1148,1149,1152,1154],{"class":159,"line":471},[298,1150,1151],{"class":364},"        end_time",[298,1153,332],{"class":303},[298,1155,1156],{"class":307},"end,\n",[298,1158,1159],{"class":159,"line":476},[298,1160,1161],{"class":307},"    )\n",[298,1163,1164,1166,1169,1171,1173,1175,1178,1181,1184,1187,1190,1193,1196],{"class":159,"line":658},[298,1165,805],{"class":303},[298,1167,1168],{"class":479}," int",[298,1170,729],{"class":307},[298,1172,64],{"class":479},[298,1174,729],{"class":307},[298,1176,1177],{"class":479},"float",[298,1179,1180],{"class":307},"(s[",[298,1182,1183],{"class":338},"\"aggregated_value\"",[298,1185,1186],{"class":307},"]) ",[298,1188,1189],{"class":303},"for",[298,1191,1192],{"class":307}," s ",[298,1194,1195],{"class":303},"in",[298,1197,1198],{"class":307}," summaries.auto_paging_iter()))\n",[298,1200,1201],{"class":159,"line":672},[298,1202,323],{"emptyLinePlaceholder":322},[298,1204,1205,1207,1210,1212,1214,1216,1218,1220,1222,1224,1226,1229,1231,1233,1236],{"class":159,"line":696},[298,1206,597],{"class":303},[298,1208,1209],{"class":600}," reconcile",[298,1211,1083],{"class":307},[298,1213,607],{"class":479},[298,1215,1088],{"class":307},[298,1217,607],{"class":479},[298,1219,1093],{"class":307},[298,1221,613],{"class":479},[298,1223,1098],{"class":307},[298,1225,613],{"class":479},[298,1227,1228],{"class":307},", local_count: ",[298,1230,613],{"class":479},[298,1232,621],{"class":307},[298,1234,1235],{"class":479},"dict",[298,1237,627],{"class":307},[298,1239,1240,1243,1245],{"class":159,"line":702},[298,1241,1242],{"class":307},"    reported ",[298,1244,332],{"class":303},[298,1246,1247],{"class":307}," stripe_meter_total(meter_id, customer_id, start, end)\n",[298,1249,1250,1253,1255,1258,1261],{"class":159,"line":717},[298,1251,1252],{"class":307},"    drift ",[298,1254,332],{"class":303},[298,1256,1257],{"class":307}," reported ",[298,1259,1260],{"class":303},"-",[298,1262,1263],{"class":307}," local_count\n",[298,1265,1266,1268,1270,1273,1276,1279,1282,1285,1288,1291,1294,1297,1300],{"class":159,"line":723},[298,1267,805],{"class":303},[298,1269,871],{"class":307},[298,1271,1272],{"class":338},"\"stripe\"",[298,1274,1275],{"class":307},": reported, ",[298,1277,1278],{"class":338},"\"local\"",[298,1280,1281],{"class":307},": local_count, ",[298,1283,1284],{"class":338},"\"drift\"",[298,1286,1287],{"class":307},": drift, ",[298,1289,1290],{"class":338},"\"ok\"",[298,1292,1293],{"class":307},": drift ",[298,1295,1296],{"class":303},"==",[298,1298,1299],{"class":479}," 0",[298,1301,1302],{"class":307},"}\n",[14,1304,1305],{},"Run reconciliation on a schedule near period close, well before Stripe finalizes the invoice. The decision below is the whole discipline: compare the two totals, and if they disagree, fix the events while you still can. Once an invoice is paid, correcting it means a manual credit or an out-of-band charge, both of which cost you support time and customer goodwill.",[92,1307,101,1311,101,1314,101,1317,101,1324,101,1326,101,1331,101,1335,101,1339,101,1341,101,1345,101,1349,101,1354,101,1357,101,1362,101,1366,101,1370,101,1374,101,1378,101,1382,101,1386,101,1390,101,1395,101,1397,101,1401],{"viewBox":924,"role":95,"ariaLabelledBy":1308,"xmlns":99,"style":100},[1309,1310],"smb-rec-t","smb-rec-d",[103,1312,1313],{"id":1309},"Reconciliation decision before invoice finalizes",[107,1315,1316],{"id":1310},"Compare Stripe's aggregated total against the local counter; zero drift finalizes the invoice, non-zero drift branches to investigate dropped or duplicated events.",[111,1318,113,1319,101],{},[115,1320,123,1322,113],{"id":1321,"viewBox":118,"refX":119,"refY":120,"markerWidth":121,"markerHeight":121,"orient":122},"smb-rec-arrow",[125,1323],{"d":127,"fill":128},[130,1325],{"x":132,"y":132,"width":133,"height":943,"fill":135},[130,1327],{"x":1328,"y":946,"width":1329,"height":1330,"rx":949,"fill":135,"stroke":167,"style":144},"30","180","52",[146,1332,1334],{"x":181,"y":1333,"fill":150,"style":985},"63","Local counter",[146,1336,1338],{"x":181,"y":1337,"fill":128,"style":985},"81","1,000,000",[130,1340],{"x":1328,"y":181,"width":1329,"height":1330,"rx":949,"fill":135,"stroke":196,"style":144},[146,1342,1344],{"x":181,"y":1343,"fill":150,"style":985},"143","Stripe aggregate",[146,1346,1348],{"x":181,"y":1347,"fill":128,"style":985},"161","999,880",[159,1350],{"x1":1351,"y1":1352,"x2":957,"y2":1352,"stroke":128,"style":1353},"210","106","stroke-width:2;marker-end:url(#smb-rec-arrow);",[125,1355],{"d":1356,"fill":135,"stroke":182,"style":144},"M290 106 L370 76 L450 106 L370 136 z",[146,1358,1361],{"x":1359,"y":1360,"fill":150,"style":985},"370","102","drift",[146,1363,1365],{"x":1359,"y":1364,"fill":128,"style":156},"118","== 0 ?",[159,1367],{"x1":1368,"y1":1352,"x2":1369,"y2":1352,"stroke":128,"style":1353},"450","520",[146,1371,1373],{"x":1372,"y":149,"fill":128,"style":156},"485","yes",[130,1375],{"x":1369,"y":1376,"width":1377,"height":1330,"rx":949,"fill":135,"stroke":143,"style":144},"80","170",[146,1379,1381],{"x":967,"y":1380,"fill":150,"style":985},"103","Let invoice",[146,1383,1385],{"x":967,"y":1384,"fill":150,"style":985},"121","finalize",[159,1387],{"x1":1359,"y1":1388,"x2":1359,"y2":1389,"stroke":128,"style":1353},"136","205",[146,1391,1394],{"x":1392,"y":1393,"fill":128,"style":156},"400","172","no (-120)",[130,1396],{"x":163,"y":1351,"width":971,"height":141,"rx":949,"fill":135,"stroke":196,"style":144},[146,1398,1400],{"x":1359,"y":1399,"fill":150,"style":985},"234","Investigate before finalize",[146,1402,1404],{"x":1359,"y":1403,"fill":128,"style":156},"253","dropped reports or dup identifiers",[42,1406,1408],{"id":1407},"failure-modes-that-quietly-cost-you-money","Failure modes that quietly cost you money",[14,1410,1411,1412,1415,1416,1418,1419,1422,1423,1426,1427,1429,1430,1433],{},"Metered billing fails silently, which is what makes it dangerous. Four modes account for almost every incident. ",[18,1413,1414],{},"Dropped events",": your ",[54,1417,1010],{}," raised, you logged it, and nobody topped up the report — the customer is under-billed and you never sent the revenue. ",[18,1420,1421],{},"Duplicate identifiers",": two code paths generated the same request id or you fell back to a timestamp under load, so Stripe deduped legitimate distinct events and under-counted, or a non-unique identifier let a replay through and over-counted. ",[18,1424,1425],{},"Wrong customer mapping",": an API key mapped to the wrong ",[54,1428,497],{},", so one account's usage lands on another's invoice — the kind of bug that turns into a refund and an apology. ",[18,1431,1432],{},"Clock and window edges",": an event reported at 23:59:59 on the last day of the period can land in the next window depending on Stripe's timestamping, shifting revenue between invoices.",[14,1435,1436,1437,1439,1440,1444],{},"The defense against all four is the same pair of habits you have already built: a local event log that is the source of truth, and a reconciliation job that runs before every finalize. Treat the reconciliation ",[54,1438,1361],{}," as a monitored metric, not a script you run by hand — alert on any non-zero value the way you would alert on a 500. If your free tier feeds into the same meter, the abuse patterns in ",[23,1441,1443],{"href":1442},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fpreventing-free-tier-abuse\u002F","preventing free-tier abuse"," are the same events distorting your paid totals, so the two problems share a fix.",[42,1446,1448],{"id":1447},"cost-and-performance-at-scale","Cost and performance at scale",[14,1450,1451],{},"Reporting meter events to Stripe is free — there is no per-event fee — so the cost of metered billing is not on your Stripe bill, it is in latency and compute. That is exactly why Step 2 keeps the report off the request path. Reporting synchronously means every billable request waits on a Stripe API round trip, which in practice adds 100–150 ms to your p95 for zero customer benefit. Moving the report to a background task or queue erases that, and batching in the worker keeps API-call volume flat as traffic grows.",[92,1453,101,1458,101,1461,101,1464,101,1466,101,1469,101,1472,101,1476,101,1479,101,1482,101,1486,101,1492,101,1496,101,1500,101,1505,101,1509,101,1513,101,1517,101,1521],{"viewBox":1454,"role":95,"ariaLabelledBy":1455,"xmlns":99,"style":100},"0 0 720 280",[1456,1457],"smb-cost-t","smb-cost-d",[103,1459,1460],{"id":1456},"p95 request latency by reporting strategy",[107,1462,1463],{"id":1457},"Synchronous reporting pushes p95 latency to about 185 ms; a background task drops it to about 58 ms; a batched queue worker to about 52 ms.",[130,1465],{"x":132,"y":132,"width":133,"height":971,"fill":135},[159,1467],{"x1":195,"y1":1328,"x2":195,"y2":1468,"stroke":972,"style":992},"220",[159,1470],{"x1":195,"y1":1468,"x2":1471,"y2":1468,"stroke":972,"style":992},"690",[146,1473,134],{"x":1474,"y":946,"fill":128,"style":1475},"66","text-anchor:end;font-size:11;font-family:var(--font-sans);",[146,1477,162],{"x":1474,"y":1478,"fill":128,"style":1475},"130",[146,1480,132],{"x":1474,"y":1481,"fill":128,"style":1475},"216",[146,1483,1485],{"x":946,"y":1478,"fill":128,"style":156,"transform":1484},"rotate(-90 40 130)","p95 ms",[130,1487],{"x":948,"y":1488,"width":1489,"height":1490,"rx":1491,"fill":196},"44","110","176","4",[146,1493,1495],{"x":1389,"y":1494,"fill":150,"style":985},"240","Synchronous",[146,1497,1499],{"x":1389,"y":1498,"fill":150,"style":985},"38","185 ms",[130,1501],{"x":1502,"y":1503,"width":1489,"height":1504,"rx":1491,"fill":167},"320","165","55",[146,1506,1508],{"x":1507,"y":1494,"fill":150,"style":985},"375","Background",[146,1510,1512],{"x":1507,"y":1511,"fill":150,"style":985},"159","58 ms",[130,1514],{"x":1515,"y":1377,"width":1489,"height":1516,"rx":1491,"fill":143},"490","50",[146,1518,1520],{"x":1519,"y":1494,"fill":150,"style":985},"545","Batched queue",[146,1522,1524],{"x":1519,"y":1523,"fill":150,"style":985},"164","52 ms",[14,1526,1527,1528,1531,1532,1536],{},"At five million billable requests a month, the compute to report every event through a batching worker is a rounding error — a fraction of a vCPU-hour — as long as you are not paying for it in tail latency. The real scaling concern is reconciliation read volume: ",[54,1529,1530],{},"list_event_summaries"," paginates, so a job that reconciles thousands of customers near period close should run incrementally through the period, not in one burst at midnight. Feed those same aggregates into a ",[23,1533,1535],{"href":1534},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Ftracking-api-usage-and-analytics\u002Fbuilding-a-customer-usage-dashboard\u002F","customer usage dashboard"," so customers can watch their own meter climb — an in-product usage view is the cheapest support-ticket deflector metered billing has.",[42,1538,1540],{"id":1539},"configuration-reference","Configuration reference",[1542,1543,1544,1560],"table",{},[1545,1546,1547],"thead",{},[1548,1549,1550,1554,1557],"tr",{},[1551,1552,1553],"th",{},"Env var",[1551,1555,1556],{},"Default",[1551,1558,1559],{},"Production note",[1561,1562,1563,1577,1594],"tbody",{},[1548,1564,1565,1571,1574],{},[1566,1567,1568],"td",{},[54,1569,1570],{},"STRIPE_SECRET_KEY",[1566,1572,1573],{},"none",[1566,1575,1576],{},"Server-side only; never log it",[1548,1578,1579,1584,1588],{},[1566,1580,1581],{},[54,1582,1583],{},"METER_EVENT_NAME",[1566,1585,1586],{},[54,1587,60],{},[1566,1589,1590,1591,1593],{},"Must match the meter's ",[54,1592,56],{}," exactly",[1548,1595,1596,1601,1606],{},[1566,1597,1598],{},[54,1599,1600],{},"DEMO_STRIPE_CUSTOMER_ID",[1566,1602,1603],{},[54,1604,1605],{},"cus_demo",[1566,1607,1608],{},"Replace with a real per-account lookup",[42,1610,1612],{"id":1611},"builder-verdict","Builder verdict",[14,1614,1615,1616,1619,1620,1622],{},"Use the Billing Meters API, not the old usage-record endpoints — Stripe deprecated those, and tutorials that still show ",[54,1617,1618],{},"SubscriptionItem.create_usage_record"," will lead you down a dead end. Get four things right and metered billing is boring in the good way: choose the aggregation that matches your billing axis before you write code, report events off the request path, make every report idempotent with a stable ",[54,1621,513],{},", and reconcile Stripe's aggregate against your own counter before each invoice finalizes. Skip reconciliation and you will not discover dropped or duplicated events until a customer disputes a bill — which is the most expensive possible time to learn your meter was wrong.",[42,1624,1626],{"id":1625},"faq","FAQ",[14,1628,1629,1632,1633,1636,1637,1640],{},[18,1630,1631],{},"Does Stripe still use usage records for metered billing?","\nNo. Stripe replaced the older ",[54,1634,1635],{},"SubscriptionItem"," usage-record flow with the Billing Meters API — meters and meter events. New integrations should create a meter, bind a metered price to it, and report ",[54,1638,1639],{},"MeterEvent"," objects. Existing usage-record integrations should plan a migration, because the deprecated path will not receive new features and eventually stops accepting reports.",[14,1642,1643,1646],{},[18,1644,1645],{},"How much does metered billing cost to run at five million requests a month?","\nAlmost nothing on Stripe's side — reporting meter events carries no per-event fee. Your cost is compute and latency, and it stays low as long as you report off the request path and batch events in a worker: a fraction of a vCPU-hour for the reporting, plus modest read volume during reconciliation. Report synchronously and you instead pay 100–150 ms of p95 latency on every billable request.",[14,1648,1649,1652,1653,1655],{},[18,1650,1651],{},"How do I avoid double-billing on retries?","\nPass a stable, unique ",[54,1654,513],{}," on every meter event — your own request id, never a timestamp. Stripe deduplicates events that share an identifier within its window, so a replayed report is counted once. Keep a local copy of reported events so reconciliation can catch any identifier that leaked through non-unique.",[14,1657,1658,1661,1662,1664],{},[18,1659,1660],{},"Should I report usage synchronously in the request?","\nNo. Report meter events in a background task or queue after the work succeeds, so a slow or failing billing call never adds latency to the API response. A reporting failure should log and fall through to reconciliation, not break the request. Above a few hundred requests per second, move from ",[54,1663,1020],{}," to a durable queue so reports survive a restart.",[14,1666,1667,1670,1671,1673],{},[18,1668,1669],{},"How do I catch metering errors before they hit a customer's invoice?","\nReconcile near period close: pull Stripe's aggregated meter total for each customer with ",[54,1672,1530],{}," and compare it to your local counter. Any drift signals dropped or duplicated events; fix it before the invoice finalizes, since post-payment corrections require manual credits or charges. Treat drift as a monitored metric and alert on any non-zero value.",[42,1675,1677],{"id":1676},"related","Related",[14,1679,1680],{},[18,1681,1682],{},"Same track:",[1684,1685,1686,1692,1698,1704],"ul",{},[1687,1688,1689,1691],"li",{},[23,1690,26],{"href":25}," — the guide this page sits under, framing where metered billing fits your tiers.",[1687,1693,1694,1697],{},[23,1695,1696],{"href":35},"How to Charge for API Access Using Stripe"," — the checkout and subscription setup you need before metering.",[1687,1699,1700,1703],{},[23,1701,1702],{"href":83},"Usage-Based vs Seat-Based Pricing"," — decide the billing axis your meter counts along.",[1687,1705,1706,1709],{},[23,1707,1708],{"href":264},"Calculating Cost per API Request"," — set meter prices above your true marginal cost.",[14,1711,1712],{},[18,1713,1714],{},"Other tracks:",[1684,1716,1717,1723,1729],{},[1687,1718,1719,1722],{},[23,1720,1721],{"href":88},"Logging API Usage Events to Postgres"," — the local source of truth you reconcile the meter against.",[1687,1724,1725,1728],{},[23,1726,1727],{"href":501},"Testing Stripe Integrations with Test Clocks"," — drive a full billing period before real events flow.",[1687,1730,1731,1735],{},[23,1732,1734],{"href":1733},"\u002Fautomating-side-hustle-operations-with-apis\u002Fprocessing-webhooks-with-python\u002Fverifying-stripe-webhook-signatures\u002F","Verifying Stripe Webhook Signatures"," — secure the invoice and payment webhooks that close the loop.",[1737,1738,1739],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":294,"searchDepth":311,"depth":311,"links":1741},[1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752],{"id":44,"depth":311,"text":45},{"id":205,"depth":311,"text":206},{"id":269,"depth":311,"text":270},{"id":506,"depth":311,"text":507},{"id":1032,"depth":311,"text":1033},{"id":1407,"depth":311,"text":1408},{"id":1447,"depth":311,"text":1448},{"id":1539,"depth":311,"text":1540},{"id":1611,"depth":311,"text":1612},{"id":1625,"depth":311,"text":1626},{"id":1676,"depth":311,"text":1677},"Configure Stripe Billing Meters for usage-based Python APIs: create meters, report usage events from FastAPI, reconcile invoices, and avoid revenue drift.","md",{"pageTitle":1756,"type":1757,"datePublished":1758,"dateModified":1759},"Stripe Metered Billing Configuration in Python","article","2026-06-18","2026-07-23","\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fstripe-metered-billing-configuration",{"title":5,"description":1753},"building-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fstripe-metered-billing-configuration\u002Findex","3i1cFH_k_K7y3VwbjkkZ1HV_8auKqoZwc4ZIrWafoLA",{"@context":1765,"@type":1766,"mainEntity":1767},"https:\u002F\u002Fschema.org","FAQPage",[1768,1773,1776,1779,1782],{"@type":1769,"name":1631,"acceptedAnswer":1770},"Question",{"@type":1771,"text":1772},"Answer","No. Stripe replaced the older SubscriptionItem usage-record flow with the Billing Meters API — meters and meter events. New integrations should create a meter, bind a metered price to it, and report MeterEvent objects. Existing usage-record integrations should plan a migration, because the deprecated path will not receive new features and eventually stops accepting reports.",{"@type":1769,"name":1645,"acceptedAnswer":1774},{"@type":1771,"text":1775},"Almost nothing on Stripe's side — reporting meter events carries no per-event fee. Your cost is compute and latency, and it stays low as long as you report off the request path and batch events in a worker: a fraction of a vCPU-hour for the reporting, plus modest read volume during reconciliation. Report synchronously and you instead pay 100–150 ms of p95 latency on every billable request.",{"@type":1769,"name":1651,"acceptedAnswer":1777},{"@type":1771,"text":1778},"Pass a stable, unique identifier on every meter event — your own request id, never a timestamp. Stripe deduplicates events that share an identifier within its window, so a replayed report is counted once. Keep a local copy of reported events so reconciliation can catch any identifier that leaked through non-unique.",{"@type":1769,"name":1660,"acceptedAnswer":1780},{"@type":1771,"text":1781},"No. Report meter events in a background task or queue after the work succeeds, so a slow or failing billing call never adds latency to the API response. A reporting failure should log and fall through to reconciliation, not break the request. Above a few hundred requests per second, move from BackgroundTasks to a durable queue so reports survive a restart.",{"@type":1769,"name":1669,"acceptedAnswer":1783},{"@type":1771,"text":1784},"Reconcile near period close: pull Stripe's aggregated meter total for each customer with list_event_summaries and compare it to your local counter. Any drift signals dropped or duplicated events; fix it before the invoice finalizes, since post-payment corrections require manual credits or charges. Treat drift as a monitored metric and alert on any non-zero value.",1784887028387]