[{"data":1,"prerenderedAt":1871},["ShallowReactive",2],{"page-\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fhow-to-charge-for-api-access-using-stripe\u002F":3,"faq-schema-\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fhow-to-charge-for-api-access-using-stripe\u002F":1853},{"id":4,"title":5,"body":6,"description":16,"extension":1847,"meta":1848,"navigation":126,"path":1849,"seo":1850,"stem":1851,"__hash__":1852},"content\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fhow-to-charge-for-api-access-using-stripe\u002Findex.md","How to Charge for API Access Using Stripe (Python Implementation Guide)",{"type":7,"value":8,"toc":1838},"minimark",[9,13,17,23,39,42,47,50,59,61,65,77,478,483,511,513,517,520,900,904,926,928,932,935,1303,1307,1322,1324,1328,1336,1722,1726,1745,1747,1751,1788,1790,1794,1804,1814,1820,1834],[10,11,5],"h1",{"id":12},"how-to-charge-for-api-access-using-stripe-python-implementation-guide",[14,15,16],"p",{},"Monetizing an API requires more than slapping a price tag on an endpoint. You need secure payment routing, real-time access control, and latency-free request validation. This guide delivers a production-ready Python implementation for charging API access via Stripe. You will learn how to map pricing tiers to Stripe products, generate cryptographically secure API keys, enforce subscription validation via async middleware, and automate access revocation through signed webhooks.",[14,18,19],{},[20,21,22],"strong",{},"Key Implementation Points:",[24,25,26,30,33,36],"ul",{},[27,28,29],"li",{},"Map API tiers to Stripe Products & Prices before writing code",[27,31,32],{},"Implement secure API key generation tied to active subscriptions",[27,34,35],{},"Handle Stripe webhooks for real-time access control and revocation",[27,37,38],{},"Enforce usage limits via async middleware and Redis caching",[40,41],"hr",{},[43,44,46],"h2",{"id":45},"architecture-pricing-model-setup","Architecture & Pricing Model Setup",[14,48,49],{},"Before touching the Stripe SDK, you must align your technical rate limits with your business pricing strategy. Decide between flat-rate subscriptions (predictable MRR, simpler enforcement) and metered usage pricing (scales with consumption, requires precise tracking).",[14,51,52,53,58],{},"Define your Stripe Products and Prices in the dashboard first. Each price should map to a specific tier with explicit technical constraints: request caps, concurrency limits, and feature gates. Store these mappings in a configuration table or environment variables so your Python code can enforce them without hardcoding. This foundational alignment ensures your billing infrastructure scales alongside your product, a critical step when operating within the broader ",[54,55,57],"a",{"href":56},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002F","Building & Monetizing API-Driven Micro-SaaS"," framework.",[40,60],{},[43,62,64],{"id":63},"stripe-checkout-session-customer-onboarding","Stripe Checkout Session & Customer Onboarding",[14,66,67,68,72,73,76],{},"Generate secure payment sessions that capture the Stripe ",[69,70,71],"code",{},"customer_id"," and ",[69,74,75],{},"subscription_id"," for downstream tracking. Always use environment variables for credentials and wrap SDK calls in explicit error handling.",[78,79,84],"pre",{"className":80,"code":81,"language":82,"meta":83,"style":83},"language-python shiki shiki-themes github-light github-dark","import os\nimport stripe\nfrom fastapi import HTTPException\n\n# Initialize Stripe SDK with explicit API version and timeout defaults\nstripe.api_key = os.getenv(\"STRIPE_SECRET_KEY\")\nstripe.api_version = \"2023-10-16\"\n\ndef create_checkout_session(customer_email: str, price_id: str) -> str:\n \"\"\"\n Creates a Stripe Checkout session for API subscription.\n Returns the redirect URL to complete payment.\n \"\"\"\n try:\n session = stripe.checkout.Session.create(\n payment_method_types=[\"card\"],\n customer_email=customer_email,\n line_items=[{\"price\": price_id, \"quantity\": 1}],\n mode=\"subscription\",\n success_url=f\"{os.getenv('APP_URL')}\u002Fdashboard?session_id={{CHECKOUT_SESSION_ID}}\",\n cancel_url=f\"{os.getenv('APP_URL')}\u002Fpricing\",\n metadata={\"source\": \"api_subscription\"},\n )\n return session.url\n except stripe.error.StripeError as e:\n raise HTTPException(status_code=502, detail=f\"Stripe session creation failed: {e.user_message}\")\n","python","",[69,85,86,99,107,121,128,135,154,165,170,200,206,212,218,223,231,242,260,271,300,314,359,386,407,413,422,437],{"__ignoreMap":83},[87,88,91,95],"span",{"class":89,"line":90},"line",1,[87,92,94],{"class":93},"szBVR","import",[87,96,98],{"class":97},"sVt8B"," os\n",[87,100,102,104],{"class":89,"line":101},2,[87,103,94],{"class":93},[87,105,106],{"class":97}," stripe\n",[87,108,110,113,116,118],{"class":89,"line":109},3,[87,111,112],{"class":93},"from",[87,114,115],{"class":97}," fastapi ",[87,117,94],{"class":93},[87,119,120],{"class":97}," HTTPException\n",[87,122,124],{"class":89,"line":123},4,[87,125,127],{"emptyLinePlaceholder":126},true,"\n",[87,129,131],{"class":89,"line":130},5,[87,132,134],{"class":133},"sJ8bj","# Initialize Stripe SDK with explicit API version and timeout defaults\n",[87,136,138,141,144,147,151],{"class":89,"line":137},6,[87,139,140],{"class":97},"stripe.api_key ",[87,142,143],{"class":93},"=",[87,145,146],{"class":97}," os.getenv(",[87,148,150],{"class":149},"sZZnC","\"STRIPE_SECRET_KEY\"",[87,152,153],{"class":97},")\n",[87,155,157,160,162],{"class":89,"line":156},7,[87,158,159],{"class":97},"stripe.api_version ",[87,161,143],{"class":93},[87,163,164],{"class":149}," \"2023-10-16\"\n",[87,166,168],{"class":89,"line":167},8,[87,169,127],{"emptyLinePlaceholder":126},[87,171,173,176,180,183,187,190,192,195,197],{"class":89,"line":172},9,[87,174,175],{"class":93},"def",[87,177,179],{"class":178},"sScJk"," create_checkout_session",[87,181,182],{"class":97},"(customer_email: ",[87,184,186],{"class":185},"sj4cs","str",[87,188,189],{"class":97},", price_id: ",[87,191,186],{"class":185},[87,193,194],{"class":97},") -> ",[87,196,186],{"class":185},[87,198,199],{"class":97},":\n",[87,201,203],{"class":89,"line":202},10,[87,204,205],{"class":149}," \"\"\"\n",[87,207,209],{"class":89,"line":208},11,[87,210,211],{"class":149}," Creates a Stripe Checkout session for API subscription.\n",[87,213,215],{"class":89,"line":214},12,[87,216,217],{"class":149}," Returns the redirect URL to complete payment.\n",[87,219,221],{"class":89,"line":220},13,[87,222,205],{"class":149},[87,224,226,229],{"class":89,"line":225},14,[87,227,228],{"class":93}," try",[87,230,199],{"class":97},[87,232,234,237,239],{"class":89,"line":233},15,[87,235,236],{"class":97}," session ",[87,238,143],{"class":93},[87,240,241],{"class":97}," stripe.checkout.Session.create(\n",[87,243,245,249,251,254,257],{"class":89,"line":244},16,[87,246,248],{"class":247},"s4XuR"," payment_method_types",[87,250,143],{"class":93},[87,252,253],{"class":97},"[",[87,255,256],{"class":149},"\"card\"",[87,258,259],{"class":97},"],\n",[87,261,263,266,268],{"class":89,"line":262},17,[87,264,265],{"class":247}," customer_email",[87,267,143],{"class":93},[87,269,270],{"class":97},"customer_email,\n",[87,272,274,277,279,282,285,288,291,294,297],{"class":89,"line":273},18,[87,275,276],{"class":247}," line_items",[87,278,143],{"class":93},[87,280,281],{"class":97},"[{",[87,283,284],{"class":149},"\"price\"",[87,286,287],{"class":97},": price_id, ",[87,289,290],{"class":149},"\"quantity\"",[87,292,293],{"class":97},": ",[87,295,296],{"class":185},"1",[87,298,299],{"class":97},"}],\n",[87,301,303,306,308,311],{"class":89,"line":302},19,[87,304,305],{"class":247}," mode",[87,307,143],{"class":93},[87,309,310],{"class":149},"\"subscription\"",[87,312,313],{"class":97},",\n",[87,315,317,320,322,325,328,331,334,337,340,343,346,349,352,355,357],{"class":89,"line":316},20,[87,318,319],{"class":247}," success_url",[87,321,143],{"class":93},[87,323,324],{"class":93},"f",[87,326,327],{"class":149},"\"",[87,329,330],{"class":185},"{",[87,332,333],{"class":97},"os.getenv(",[87,335,336],{"class":149},"'APP_URL'",[87,338,339],{"class":97},")",[87,341,342],{"class":185},"}",[87,344,345],{"class":149},"\u002Fdashboard?session_id=",[87,347,348],{"class":185},"{{",[87,350,351],{"class":149},"CHECKOUT_SESSION_ID",[87,353,354],{"class":185},"}}",[87,356,327],{"class":149},[87,358,313],{"class":97},[87,360,362,365,367,369,371,373,375,377,379,381,384],{"class":89,"line":361},21,[87,363,364],{"class":247}," cancel_url",[87,366,143],{"class":93},[87,368,324],{"class":93},[87,370,327],{"class":149},[87,372,330],{"class":185},[87,374,333],{"class":97},[87,376,336],{"class":149},[87,378,339],{"class":97},[87,380,342],{"class":185},[87,382,383],{"class":149},"\u002Fpricing\"",[87,385,313],{"class":97},[87,387,389,392,394,396,399,401,404],{"class":89,"line":388},22,[87,390,391],{"class":247}," metadata",[87,393,143],{"class":93},[87,395,330],{"class":97},[87,397,398],{"class":149},"\"source\"",[87,400,293],{"class":97},[87,402,403],{"class":149},"\"api_subscription\"",[87,405,406],{"class":97},"},\n",[87,408,410],{"class":89,"line":409},23,[87,411,412],{"class":97}," )\n",[87,414,416,419],{"class":89,"line":415},24,[87,417,418],{"class":93}," return",[87,420,421],{"class":97}," session.url\n",[87,423,425,428,431,434],{"class":89,"line":424},25,[87,426,427],{"class":93}," except",[87,429,430],{"class":97}," stripe.error.StripeError ",[87,432,433],{"class":93},"as",[87,435,436],{"class":97}," e:\n",[87,438,440,443,446,449,451,454,457,460,462,464,467,469,472,474,476],{"class":89,"line":439},26,[87,441,442],{"class":93}," raise",[87,444,445],{"class":97}," HTTPException(",[87,447,448],{"class":247},"status_code",[87,450,143],{"class":93},[87,452,453],{"class":185},"502",[87,455,456],{"class":97},", ",[87,458,459],{"class":247},"detail",[87,461,143],{"class":93},[87,463,324],{"class":93},[87,465,466],{"class":149},"\"Stripe session creation failed: ",[87,468,330],{"class":185},[87,470,471],{"class":97},"e.user_message",[87,473,342],{"class":185},[87,475,327],{"class":149},[87,477,153],{"class":97},[14,479,480],{},[20,481,482],{},"Implementation Notes:",[24,484,485,497,504],{},[27,486,487,488,72,490,492,493,496],{},"Persist ",[69,489,71],{},[69,491,75],{}," in your database immediately after receiving the ",[69,494,495],{},"checkout.session.completed"," webhook.",[27,498,499,500,503],{},"Use ",[69,501,502],{},"metadata"," to track acquisition channels or tier identifiers.",[27,505,506,507,510],{},"Never expose ",[69,508,509],{},"STRIPE_SECRET_KEY"," in client-side code or logs.",[40,512],{},[43,514,516],{"id":515},"api-key-generation-request-validation-middleware","API Key Generation & Request Validation Middleware",[14,518,519],{},"Issue cryptographically secure keys post-payment and validate them on every incoming request without blocking the event loop. Cache subscription status in Redis to avoid Stripe API latency on hot paths.",[78,521,523],{"className":80,"code":522,"language":82,"meta":83,"style":83},"import os\nimport secrets\nimport redis.asyncio as redis\nfrom fastapi import Depends, HTTPException, Header, Request\nfrom typing import Optional\n\n# Redis connection with connection pooling and timeout\nredis_client = redis.Redis.from_url(\n os.getenv(\"REDIS_URL\", \"redis:\u002F\u002Flocalhost:6379\u002F0\"),\n decode_responses=True,\n socket_connect_timeout=2,\n socket_timeout=2\n)\n\nasync def validate_api_key(x_api_key: str = Header(...)) -> dict:\n \"\"\"\n FastAPI dependency that validates API keys against Redis cache.\n Returns tier metadata if valid, raises 401\u002F403 otherwise.\n \"\"\"\n cache_key = f\"sub:{x_api_key}\"\n \n try:\n cached_status = await redis_client.get(cache_key)\n except redis.ConnectionError:\n raise HTTPException(status_code=503, detail=\"Cache service unavailable\")\n \n if not cached_status:\n # Cache miss: fallback to DB lookup (implementation omitted for brevity)\n raise HTTPException(status_code=401, detail=\"Invalid or expired API key\")\n \n if cached_status != \"active\":\n raise HTTPException(status_code=403, detail=\"Subscription inactive or past due\")\n \n return {\"key\": x_api_key, \"status\": \"active\"}\n",[69,524,525,531,538,550,561,573,577,582,592,607,619,631,641,645,649,682,686,691,696,700,723,728,734,747,754,778,782,794,800,825,830,845,870,875],{"__ignoreMap":83},[87,526,527,529],{"class":89,"line":90},[87,528,94],{"class":93},[87,530,98],{"class":97},[87,532,533,535],{"class":89,"line":101},[87,534,94],{"class":93},[87,536,537],{"class":97}," secrets\n",[87,539,540,542,545,547],{"class":89,"line":109},[87,541,94],{"class":93},[87,543,544],{"class":97}," redis.asyncio ",[87,546,433],{"class":93},[87,548,549],{"class":97}," redis\n",[87,551,552,554,556,558],{"class":89,"line":123},[87,553,112],{"class":93},[87,555,115],{"class":97},[87,557,94],{"class":93},[87,559,560],{"class":97}," Depends, HTTPException, Header, Request\n",[87,562,563,565,568,570],{"class":89,"line":130},[87,564,112],{"class":93},[87,566,567],{"class":97}," typing ",[87,569,94],{"class":93},[87,571,572],{"class":97}," Optional\n",[87,574,575],{"class":89,"line":137},[87,576,127],{"emptyLinePlaceholder":126},[87,578,579],{"class":89,"line":156},[87,580,581],{"class":133},"# Redis connection with connection pooling and timeout\n",[87,583,584,587,589],{"class":89,"line":167},[87,585,586],{"class":97},"redis_client ",[87,588,143],{"class":93},[87,590,591],{"class":97}," redis.Redis.from_url(\n",[87,593,594,596,599,601,604],{"class":89,"line":172},[87,595,146],{"class":97},[87,597,598],{"class":149},"\"REDIS_URL\"",[87,600,456],{"class":97},[87,602,603],{"class":149},"\"redis:\u002F\u002Flocalhost:6379\u002F0\"",[87,605,606],{"class":97},"),\n",[87,608,609,612,614,617],{"class":89,"line":202},[87,610,611],{"class":247}," decode_responses",[87,613,143],{"class":93},[87,615,616],{"class":185},"True",[87,618,313],{"class":97},[87,620,621,624,626,629],{"class":89,"line":208},[87,622,623],{"class":247}," socket_connect_timeout",[87,625,143],{"class":93},[87,627,628],{"class":185},"2",[87,630,313],{"class":97},[87,632,633,636,638],{"class":89,"line":214},[87,634,635],{"class":247}," socket_timeout",[87,637,143],{"class":93},[87,639,640],{"class":185},"2\n",[87,642,643],{"class":89,"line":220},[87,644,153],{"class":97},[87,646,647],{"class":89,"line":225},[87,648,127],{"emptyLinePlaceholder":126},[87,650,651,654,657,660,663,665,668,671,674,677,680],{"class":89,"line":233},[87,652,653],{"class":93},"async",[87,655,656],{"class":93}," def",[87,658,659],{"class":178}," validate_api_key",[87,661,662],{"class":97},"(x_api_key: ",[87,664,186],{"class":185},[87,666,667],{"class":93}," =",[87,669,670],{"class":97}," Header(",[87,672,673],{"class":185},"...",[87,675,676],{"class":97},")) -> ",[87,678,679],{"class":185},"dict",[87,681,199],{"class":97},[87,683,684],{"class":89,"line":244},[87,685,205],{"class":149},[87,687,688],{"class":89,"line":262},[87,689,690],{"class":149}," FastAPI dependency that validates API keys against Redis cache.\n",[87,692,693],{"class":89,"line":273},[87,694,695],{"class":149}," Returns tier metadata if valid, raises 401\u002F403 otherwise.\n",[87,697,698],{"class":89,"line":302},[87,699,205],{"class":149},[87,701,702,705,707,710,713,715,718,720],{"class":89,"line":316},[87,703,704],{"class":97}," cache_key ",[87,706,143],{"class":93},[87,708,709],{"class":93}," f",[87,711,712],{"class":149},"\"sub:",[87,714,330],{"class":185},[87,716,717],{"class":97},"x_api_key",[87,719,342],{"class":185},[87,721,722],{"class":149},"\"\n",[87,724,725],{"class":89,"line":361},[87,726,727],{"class":97}," \n",[87,729,730,732],{"class":89,"line":388},[87,731,228],{"class":93},[87,733,199],{"class":97},[87,735,736,739,741,744],{"class":89,"line":409},[87,737,738],{"class":97}," cached_status ",[87,740,143],{"class":93},[87,742,743],{"class":93}," await",[87,745,746],{"class":97}," redis_client.get(cache_key)\n",[87,748,749,751],{"class":89,"line":415},[87,750,427],{"class":93},[87,752,753],{"class":97}," redis.ConnectionError:\n",[87,755,756,758,760,762,764,767,769,771,773,776],{"class":89,"line":424},[87,757,442],{"class":93},[87,759,445],{"class":97},[87,761,448],{"class":247},[87,763,143],{"class":93},[87,765,766],{"class":185},"503",[87,768,456],{"class":97},[87,770,459],{"class":247},[87,772,143],{"class":93},[87,774,775],{"class":149},"\"Cache service unavailable\"",[87,777,153],{"class":97},[87,779,780],{"class":89,"line":439},[87,781,727],{"class":97},[87,783,785,788,791],{"class":89,"line":784},27,[87,786,787],{"class":93}," if",[87,789,790],{"class":93}," not",[87,792,793],{"class":97}," cached_status:\n",[87,795,797],{"class":89,"line":796},28,[87,798,799],{"class":133}," # Cache miss: fallback to DB lookup (implementation omitted for brevity)\n",[87,801,803,805,807,809,811,814,816,818,820,823],{"class":89,"line":802},29,[87,804,442],{"class":93},[87,806,445],{"class":97},[87,808,448],{"class":247},[87,810,143],{"class":93},[87,812,813],{"class":185},"401",[87,815,456],{"class":97},[87,817,459],{"class":247},[87,819,143],{"class":93},[87,821,822],{"class":149},"\"Invalid or expired API key\"",[87,824,153],{"class":97},[87,826,828],{"class":89,"line":827},30,[87,829,727],{"class":97},[87,831,833,835,837,840,843],{"class":89,"line":832},31,[87,834,787],{"class":93},[87,836,738],{"class":97},[87,838,839],{"class":93},"!=",[87,841,842],{"class":149}," \"active\"",[87,844,199],{"class":97},[87,846,848,850,852,854,856,859,861,863,865,868],{"class":89,"line":847},32,[87,849,442],{"class":93},[87,851,445],{"class":97},[87,853,448],{"class":247},[87,855,143],{"class":93},[87,857,858],{"class":185},"403",[87,860,456],{"class":97},[87,862,459],{"class":247},[87,864,143],{"class":93},[87,866,867],{"class":149},"\"Subscription inactive or past due\"",[87,869,153],{"class":97},[87,871,873],{"class":89,"line":872},33,[87,874,727],{"class":97},[87,876,878,880,883,886,889,892,894,897],{"class":89,"line":877},34,[87,879,418],{"class":93},[87,881,882],{"class":97}," {",[87,884,885],{"class":149},"\"key\"",[87,887,888],{"class":97},": x_api_key, ",[87,890,891],{"class":149},"\"status\"",[87,893,293],{"class":97},[87,895,896],{"class":149},"\"active\"",[87,898,899],{"class":97},"}\n",[14,901,902],{},[20,903,482],{},[24,905,906,913,916],{},[27,907,908,909,912],{},"Generate keys using ",[69,910,911],{},"secrets.token_urlsafe(32)"," upon successful subscription creation.",[27,914,915],{},"Set Redis TTL to 5–15 minutes. Invalidate on subscription changes via webhooks.",[27,917,499,918,921,922,925],{},[69,919,920],{},"Header(...)"," to enforce the ",[69,923,924],{},"X-API-Key"," requirement at the framework level.",[40,927],{},[43,929,931],{"id":930},"webhook-handling-real-time-access-control","Webhook Handling & Real-Time Access Control",[14,933,934],{},"Automate access provisioning and revocation by listening to Stripe lifecycle events. Always verify webhook signatures to prevent spoofed payment events from compromising your system.",[78,936,938],{"className":80,"code":937,"language":82,"meta":83,"style":83},"import os\nimport stripe\nfrom fastapi import Request, Response, HTTPException\n\nSTRIPE_WEBHOOK_SECRET = os.getenv(\"STRIPE_WEBHOOK_SECRET\")\n\n@app.post(\"\u002Fwebhooks\u002Fstripe\")\nasync def stripe_webhook(request: Request) -> Response:\n payload = await request.body()\n sig_header = request.headers.get(\"stripe-signature\")\n \n if not sig_header:\n raise HTTPException(status_code=400, detail=\"Missing signature header\")\n \n try:\n event = stripe.Webhook.construct_event(\n payload, sig_header, STRIPE_WEBHOOK_SECRET\n )\n except ValueError as e:\n raise HTTPException(status_code=400, detail=f\"Invalid payload: {e}\")\n except stripe.error.SignatureVerificationError as e:\n raise HTTPException(status_code=400, detail=f\"Signature verification failed: {e}\")\n \n # Route events to handlers\n if event.type == \"customer.subscription.deleted\":\n sub_id = event.data.object.id\n # TODO: Atomic DB update to revoke all API keys linked to sub_id\n # await db.execute(\"UPDATE api_keys SET is_active = false WHERE subscription_id = :sub\", {\"sub\": sub_id})\n \n elif event.type == \"invoice.payment_failed\":\n cust_id = event.data.object.customer\n # TODO: Notify user, mark subscription as 'past_due', enforce grace period\n \n return Response(status_code=200)\n",[69,939,940,946,952,963,967,981,985,998,1010,1022,1037,1041,1050,1074,1078,1084,1094,1102,1106,1118,1152,1163,1196,1200,1205,1220,1230,1241,1246,1250,1264,1274,1283,1287],{"__ignoreMap":83},[87,941,942,944],{"class":89,"line":90},[87,943,94],{"class":93},[87,945,98],{"class":97},[87,947,948,950],{"class":89,"line":101},[87,949,94],{"class":93},[87,951,106],{"class":97},[87,953,954,956,958,960],{"class":89,"line":109},[87,955,112],{"class":93},[87,957,115],{"class":97},[87,959,94],{"class":93},[87,961,962],{"class":97}," Request, Response, HTTPException\n",[87,964,965],{"class":89,"line":123},[87,966,127],{"emptyLinePlaceholder":126},[87,968,969,972,974,976,979],{"class":89,"line":130},[87,970,971],{"class":185},"STRIPE_WEBHOOK_SECRET",[87,973,667],{"class":93},[87,975,146],{"class":97},[87,977,978],{"class":149},"\"STRIPE_WEBHOOK_SECRET\"",[87,980,153],{"class":97},[87,982,983],{"class":89,"line":137},[87,984,127],{"emptyLinePlaceholder":126},[87,986,987,990,993,996],{"class":89,"line":156},[87,988,989],{"class":178},"@app.post",[87,991,992],{"class":97},"(",[87,994,995],{"class":149},"\"\u002Fwebhooks\u002Fstripe\"",[87,997,153],{"class":97},[87,999,1000,1002,1004,1007],{"class":89,"line":167},[87,1001,653],{"class":93},[87,1003,656],{"class":93},[87,1005,1006],{"class":178}," stripe_webhook",[87,1008,1009],{"class":97},"(request: Request) -> Response:\n",[87,1011,1012,1015,1017,1019],{"class":89,"line":172},[87,1013,1014],{"class":97}," payload ",[87,1016,143],{"class":93},[87,1018,743],{"class":93},[87,1020,1021],{"class":97}," request.body()\n",[87,1023,1024,1027,1029,1032,1035],{"class":89,"line":202},[87,1025,1026],{"class":97}," sig_header ",[87,1028,143],{"class":93},[87,1030,1031],{"class":97}," request.headers.get(",[87,1033,1034],{"class":149},"\"stripe-signature\"",[87,1036,153],{"class":97},[87,1038,1039],{"class":89,"line":208},[87,1040,727],{"class":97},[87,1042,1043,1045,1047],{"class":89,"line":214},[87,1044,787],{"class":93},[87,1046,790],{"class":93},[87,1048,1049],{"class":97}," sig_header:\n",[87,1051,1052,1054,1056,1058,1060,1063,1065,1067,1069,1072],{"class":89,"line":220},[87,1053,442],{"class":93},[87,1055,445],{"class":97},[87,1057,448],{"class":247},[87,1059,143],{"class":93},[87,1061,1062],{"class":185},"400",[87,1064,456],{"class":97},[87,1066,459],{"class":247},[87,1068,143],{"class":93},[87,1070,1071],{"class":149},"\"Missing signature header\"",[87,1073,153],{"class":97},[87,1075,1076],{"class":89,"line":225},[87,1077,727],{"class":97},[87,1079,1080,1082],{"class":89,"line":233},[87,1081,228],{"class":93},[87,1083,199],{"class":97},[87,1085,1086,1089,1091],{"class":89,"line":244},[87,1087,1088],{"class":97}," event ",[87,1090,143],{"class":93},[87,1092,1093],{"class":97}," stripe.Webhook.construct_event(\n",[87,1095,1096,1099],{"class":89,"line":262},[87,1097,1098],{"class":97}," payload, sig_header, ",[87,1100,1101],{"class":185},"STRIPE_WEBHOOK_SECRET\n",[87,1103,1104],{"class":89,"line":273},[87,1105,412],{"class":97},[87,1107,1108,1110,1113,1116],{"class":89,"line":302},[87,1109,427],{"class":93},[87,1111,1112],{"class":185}," ValueError",[87,1114,1115],{"class":93}," as",[87,1117,436],{"class":97},[87,1119,1120,1122,1124,1126,1128,1130,1132,1134,1136,1138,1141,1143,1146,1148,1150],{"class":89,"line":316},[87,1121,442],{"class":93},[87,1123,445],{"class":97},[87,1125,448],{"class":247},[87,1127,143],{"class":93},[87,1129,1062],{"class":185},[87,1131,456],{"class":97},[87,1133,459],{"class":247},[87,1135,143],{"class":93},[87,1137,324],{"class":93},[87,1139,1140],{"class":149},"\"Invalid payload: ",[87,1142,330],{"class":185},[87,1144,1145],{"class":97},"e",[87,1147,342],{"class":185},[87,1149,327],{"class":149},[87,1151,153],{"class":97},[87,1153,1154,1156,1159,1161],{"class":89,"line":361},[87,1155,427],{"class":93},[87,1157,1158],{"class":97}," stripe.error.SignatureVerificationError ",[87,1160,433],{"class":93},[87,1162,436],{"class":97},[87,1164,1165,1167,1169,1171,1173,1175,1177,1179,1181,1183,1186,1188,1190,1192,1194],{"class":89,"line":388},[87,1166,442],{"class":93},[87,1168,445],{"class":97},[87,1170,448],{"class":247},[87,1172,143],{"class":93},[87,1174,1062],{"class":185},[87,1176,456],{"class":97},[87,1178,459],{"class":247},[87,1180,143],{"class":93},[87,1182,324],{"class":93},[87,1184,1185],{"class":149},"\"Signature verification failed: ",[87,1187,330],{"class":185},[87,1189,1145],{"class":97},[87,1191,342],{"class":185},[87,1193,327],{"class":149},[87,1195,153],{"class":97},[87,1197,1198],{"class":89,"line":409},[87,1199,727],{"class":97},[87,1201,1202],{"class":89,"line":415},[87,1203,1204],{"class":133}," # Route events to handlers\n",[87,1206,1207,1209,1212,1215,1218],{"class":89,"line":424},[87,1208,787],{"class":93},[87,1210,1211],{"class":97}," event.type ",[87,1213,1214],{"class":93},"==",[87,1216,1217],{"class":149}," \"customer.subscription.deleted\"",[87,1219,199],{"class":97},[87,1221,1222,1225,1227],{"class":89,"line":439},[87,1223,1224],{"class":97}," sub_id ",[87,1226,143],{"class":93},[87,1228,1229],{"class":97}," event.data.object.id\n",[87,1231,1232,1235,1238],{"class":89,"line":784},[87,1233,1234],{"class":133}," # ",[87,1236,1237],{"class":93},"TODO",[87,1239,1240],{"class":133},": Atomic DB update to revoke all API keys linked to sub_id\n",[87,1242,1243],{"class":89,"line":796},[87,1244,1245],{"class":133}," # await db.execute(\"UPDATE api_keys SET is_active = false WHERE subscription_id = :sub\", {\"sub\": sub_id})\n",[87,1247,1248],{"class":89,"line":802},[87,1249,727],{"class":97},[87,1251,1252,1255,1257,1259,1262],{"class":89,"line":827},[87,1253,1254],{"class":93}," elif",[87,1256,1211],{"class":97},[87,1258,1214],{"class":93},[87,1260,1261],{"class":149}," \"invoice.payment_failed\"",[87,1263,199],{"class":97},[87,1265,1266,1269,1271],{"class":89,"line":832},[87,1267,1268],{"class":97}," cust_id ",[87,1270,143],{"class":93},[87,1272,1273],{"class":97}," event.data.object.customer\n",[87,1275,1276,1278,1280],{"class":89,"line":847},[87,1277,1234],{"class":133},[87,1279,1237],{"class":93},[87,1281,1282],{"class":133},": Notify user, mark subscription as 'past_due', enforce grace period\n",[87,1284,1285],{"class":89,"line":872},[87,1286,727],{"class":97},[87,1288,1289,1291,1294,1296,1298,1301],{"class":89,"line":877},[87,1290,418],{"class":93},[87,1292,1293],{"class":97}," Response(",[87,1295,448],{"class":247},[87,1297,143],{"class":93},[87,1299,1300],{"class":185},"200",[87,1302,153],{"class":97},[14,1304,1305],{},[20,1306,482],{},[24,1308,1309,1312,1319],{},[27,1310,1311],{},"Use database transactions to prevent race conditions during status updates.",[27,1313,1314,1315,1318],{},"Return ",[69,1316,1317],{},"200 OK"," immediately after parsing to avoid Stripe retries.",[27,1320,1321],{},"Log unhandled event types for future expansion.",[40,1323],{},[43,1325,1327],{"id":1326},"usage-tracking-rate-limiting-enforcement","Usage Tracking & Rate Limiting Enforcement",[14,1329,1330,1331,1335],{},"Track API calls per tier and report metered usage back to Stripe. Implement a token-bucket or sliding-window algorithm in Redis to enforce limits at the edge. Align your technical enforcement thresholds with your ",[54,1332,1334],{"href":1333},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002F","Designing API Pricing Tiers"," strategy to prevent overage abuse while maintaining a frictionless developer experience.",[78,1337,1339],{"className":80,"code":1338,"language":82,"meta":83,"style":83},"import time\nimport stripe\nfrom fastapi import Request, HTTPException\n\nRATE_LIMIT_WINDOW = 60 # seconds\nMAX_REQUESTS_PER_WINDOW = 100 # adjust per tier\n\nasync def enforce_rate_limit(request: Request, api_key: str):\n \"\"\"\n Redis-backed sliding window rate limiter.\n Raises 429 if limit exceeded.\n \"\"\"\n key = f\"ratelimit:{api_key}:{int(time.time()) \u002F\u002F RATE_LIMIT_WINDOW}\"\n current_count = await redis_client.incr(key)\n \n if current_count == 1:\n await redis_client.expire(key, RATE_LIMIT_WINDOW)\n \n if current_count > MAX_REQUESTS_PER_WINDOW:\n raise HTTPException(status_code=429, detail=\"Rate limit exceeded. Upgrade your tier.\")\n return True\n\ndef report_metered_usage(subscription_item_id: str, quantity: int, action: str = \"increment\"):\n \"\"\"\n Reports usage to Stripe for metered billing.\n Uses idempotency keys to prevent duplicate charges.\n \"\"\"\n try:\n stripe.SubscriptionItem.create_usage_record(\n subscription_item_id,\n quantity=quantity,\n action=action,\n idempotency_key=f\"usage-{subscription_item_id}-{int(time.time())}\"\n )\n except stripe.error.StripeError as e:\n # Log error, do not crash request pipeline\n print(f\"Usage reporting failed: {e}\")\n",[69,1340,1341,1348,1354,1365,1369,1382,1395,1399,1416,1420,1425,1430,1434,1470,1482,1486,1499,1510,1514,1528,1552,1559,1563,1593,1597,1602,1607,1611,1617,1622,1627,1637,1647,1678,1682,1693,1699],{"__ignoreMap":83},[87,1342,1343,1345],{"class":89,"line":90},[87,1344,94],{"class":93},[87,1346,1347],{"class":97}," time\n",[87,1349,1350,1352],{"class":89,"line":101},[87,1351,94],{"class":93},[87,1353,106],{"class":97},[87,1355,1356,1358,1360,1362],{"class":89,"line":109},[87,1357,112],{"class":93},[87,1359,115],{"class":97},[87,1361,94],{"class":93},[87,1363,1364],{"class":97}," Request, HTTPException\n",[87,1366,1367],{"class":89,"line":123},[87,1368,127],{"emptyLinePlaceholder":126},[87,1370,1371,1374,1376,1379],{"class":89,"line":130},[87,1372,1373],{"class":185},"RATE_LIMIT_WINDOW",[87,1375,667],{"class":93},[87,1377,1378],{"class":185}," 60",[87,1380,1381],{"class":133}," # seconds\n",[87,1383,1384,1387,1389,1392],{"class":89,"line":137},[87,1385,1386],{"class":185},"MAX_REQUESTS_PER_WINDOW",[87,1388,667],{"class":93},[87,1390,1391],{"class":185}," 100",[87,1393,1394],{"class":133}," # adjust per tier\n",[87,1396,1397],{"class":89,"line":156},[87,1398,127],{"emptyLinePlaceholder":126},[87,1400,1401,1403,1405,1408,1411,1413],{"class":89,"line":167},[87,1402,653],{"class":93},[87,1404,656],{"class":93},[87,1406,1407],{"class":178}," enforce_rate_limit",[87,1409,1410],{"class":97},"(request: Request, api_key: ",[87,1412,186],{"class":185},[87,1414,1415],{"class":97},"):\n",[87,1417,1418],{"class":89,"line":172},[87,1419,205],{"class":149},[87,1421,1422],{"class":89,"line":202},[87,1423,1424],{"class":149}," Redis-backed sliding window rate limiter.\n",[87,1426,1427],{"class":89,"line":208},[87,1428,1429],{"class":149}," Raises 429 if limit exceeded.\n",[87,1431,1432],{"class":89,"line":214},[87,1433,205],{"class":149},[87,1435,1436,1439,1441,1443,1446,1448,1451,1453,1456,1459,1462,1465,1468],{"class":89,"line":220},[87,1437,1438],{"class":97}," key ",[87,1440,143],{"class":93},[87,1442,709],{"class":93},[87,1444,1445],{"class":149},"\"ratelimit:",[87,1447,330],{"class":185},[87,1449,1450],{"class":97},"api_key",[87,1452,342],{"class":185},[87,1454,1455],{"class":149},":",[87,1457,1458],{"class":185},"{int",[87,1460,1461],{"class":97},"(time.time()) ",[87,1463,1464],{"class":93},"\u002F\u002F",[87,1466,1467],{"class":185}," RATE_LIMIT_WINDOW}",[87,1469,722],{"class":149},[87,1471,1472,1475,1477,1479],{"class":89,"line":225},[87,1473,1474],{"class":97}," current_count ",[87,1476,143],{"class":93},[87,1478,743],{"class":93},[87,1480,1481],{"class":97}," redis_client.incr(key)\n",[87,1483,1484],{"class":89,"line":233},[87,1485,727],{"class":97},[87,1487,1488,1490,1492,1494,1497],{"class":89,"line":244},[87,1489,787],{"class":93},[87,1491,1474],{"class":97},[87,1493,1214],{"class":93},[87,1495,1496],{"class":185}," 1",[87,1498,199],{"class":97},[87,1500,1501,1503,1506,1508],{"class":89,"line":262},[87,1502,743],{"class":93},[87,1504,1505],{"class":97}," redis_client.expire(key, ",[87,1507,1373],{"class":185},[87,1509,153],{"class":97},[87,1511,1512],{"class":89,"line":273},[87,1513,727],{"class":97},[87,1515,1516,1518,1520,1523,1526],{"class":89,"line":302},[87,1517,787],{"class":93},[87,1519,1474],{"class":97},[87,1521,1522],{"class":93},">",[87,1524,1525],{"class":185}," MAX_REQUESTS_PER_WINDOW",[87,1527,199],{"class":97},[87,1529,1530,1532,1534,1536,1538,1541,1543,1545,1547,1550],{"class":89,"line":316},[87,1531,442],{"class":93},[87,1533,445],{"class":97},[87,1535,448],{"class":247},[87,1537,143],{"class":93},[87,1539,1540],{"class":185},"429",[87,1542,456],{"class":97},[87,1544,459],{"class":247},[87,1546,143],{"class":93},[87,1548,1549],{"class":149},"\"Rate limit exceeded. Upgrade your tier.\"",[87,1551,153],{"class":97},[87,1553,1554,1556],{"class":89,"line":361},[87,1555,418],{"class":93},[87,1557,1558],{"class":185}," True\n",[87,1560,1561],{"class":89,"line":388},[87,1562,127],{"emptyLinePlaceholder":126},[87,1564,1565,1567,1570,1573,1575,1578,1581,1584,1586,1588,1591],{"class":89,"line":409},[87,1566,175],{"class":93},[87,1568,1569],{"class":178}," report_metered_usage",[87,1571,1572],{"class":97},"(subscription_item_id: ",[87,1574,186],{"class":185},[87,1576,1577],{"class":97},", quantity: ",[87,1579,1580],{"class":185},"int",[87,1582,1583],{"class":97},", action: ",[87,1585,186],{"class":185},[87,1587,667],{"class":93},[87,1589,1590],{"class":149}," \"increment\"",[87,1592,1415],{"class":97},[87,1594,1595],{"class":89,"line":415},[87,1596,205],{"class":149},[87,1598,1599],{"class":89,"line":424},[87,1600,1601],{"class":149}," Reports usage to Stripe for metered billing.\n",[87,1603,1604],{"class":89,"line":439},[87,1605,1606],{"class":149}," Uses idempotency keys to prevent duplicate charges.\n",[87,1608,1609],{"class":89,"line":784},[87,1610,205],{"class":149},[87,1612,1613,1615],{"class":89,"line":796},[87,1614,228],{"class":93},[87,1616,199],{"class":97},[87,1618,1619],{"class":89,"line":802},[87,1620,1621],{"class":97}," stripe.SubscriptionItem.create_usage_record(\n",[87,1623,1624],{"class":89,"line":827},[87,1625,1626],{"class":97}," subscription_item_id,\n",[87,1628,1629,1632,1634],{"class":89,"line":832},[87,1630,1631],{"class":247}," quantity",[87,1633,143],{"class":93},[87,1635,1636],{"class":97},"quantity,\n",[87,1638,1639,1642,1644],{"class":89,"line":847},[87,1640,1641],{"class":247}," action",[87,1643,143],{"class":93},[87,1645,1646],{"class":97},"action,\n",[87,1648,1649,1652,1654,1656,1659,1661,1664,1666,1669,1671,1674,1676],{"class":89,"line":872},[87,1650,1651],{"class":247}," idempotency_key",[87,1653,143],{"class":93},[87,1655,324],{"class":93},[87,1657,1658],{"class":149},"\"usage-",[87,1660,330],{"class":185},[87,1662,1663],{"class":97},"subscription_item_id",[87,1665,342],{"class":185},[87,1667,1668],{"class":149},"-",[87,1670,1458],{"class":185},[87,1672,1673],{"class":97},"(time.time())",[87,1675,342],{"class":185},[87,1677,722],{"class":149},[87,1679,1680],{"class":89,"line":877},[87,1681,412],{"class":97},[87,1683,1685,1687,1689,1691],{"class":89,"line":1684},35,[87,1686,427],{"class":93},[87,1688,430],{"class":97},[87,1690,433],{"class":93},[87,1692,436],{"class":97},[87,1694,1696],{"class":89,"line":1695},36,[87,1697,1698],{"class":133}," # Log error, do not crash request pipeline\n",[87,1700,1702,1705,1707,1709,1712,1714,1716,1718,1720],{"class":89,"line":1701},37,[87,1703,1704],{"class":185}," print",[87,1706,992],{"class":97},[87,1708,324],{"class":93},[87,1710,1711],{"class":149},"\"Usage reporting failed: ",[87,1713,330],{"class":185},[87,1715,1145],{"class":97},[87,1717,342],{"class":185},[87,1719,327],{"class":149},[87,1721,153],{"class":97},[14,1723,1724],{},[20,1725,482],{},[24,1727,1728,1735,1742],{},[27,1729,1730,1731,1734],{},"Call ",[69,1732,1733],{},"report_metered_usage"," asynchronously via a background task or message queue (Celery\u002FRQ) to avoid request latency.",[27,1736,1737,1738,1741],{},"Always pass a unique ",[69,1739,1740],{},"idempotency_key"," to prevent double-billing on network retries.",[27,1743,1744],{},"Combine rate limiting with tier metadata for dynamic limit enforcement.",[40,1746],{},[43,1748,1750],{"id":1749},"common-mistakes-to-avoid","Common Mistakes to Avoid",[1752,1753,1754,1760,1766,1776,1782],"ol",{},[27,1755,1756,1759],{},[20,1757,1758],{},"Skipping webhook signature verification:"," Exposes your system to spoofed payment events that can grant unauthorized access or revoke legitimate subscriptions.",[27,1761,1762,1765],{},[20,1763,1764],{},"Making synchronous Stripe API calls on every request:"," Adds 300–800ms latency per request. Always cache subscription status and tier limits.",[27,1767,1768,1771,1772,1775],{},[20,1769,1770],{},"Hardcoding secrets in version control:"," Use ",[69,1773,1774],{},".env"," files and secret managers. Rotate keys immediately if exposed.",[27,1777,1778,1781],{},[20,1779,1780],{},"Ignoring trial and grace periods:"," Premature revocation during Stripe's dunning cycle increases churn. Implement a configurable grace window (3–7 days) before disabling keys.",[27,1783,1784,1787],{},[20,1785,1786],{},"Reporting metered usage without idempotency keys:"," Network retries will trigger duplicate usage records, leading to customer disputes and billing errors.",[40,1789],{},[43,1791,1793],{"id":1792},"faq","FAQ",[14,1795,1796,1799,1800,1803],{},[20,1797,1798],{},"How do I handle failed payments without immediately breaking API access?","\nStripe's automated dunning management retries failed charges over several days. Implement a grace period in your database logic (e.g., 3–7 days) before revoking keys. Listen for ",[69,1801,1802],{},"invoice.payment_failed"," to trigger user notifications via email or dashboard alerts.",[14,1805,1806,1809,1810,1813],{},[20,1807,1808],{},"Can I use Stripe metered billing for per-request API pricing?","\nYes. Use ",[69,1811,1812],{},"stripe.SubscriptionItem.create_usage_record()"," to report usage at the end of each billing cycle or daily. Pair this with a Redis counter to track requests per API key accurately and report in bulk to minimize API overhead.",[14,1815,1816,1819],{},[20,1817,1818],{},"How do I securely validate API keys without slowing down response times?","\nNever call the Stripe API synchronously on every request. Cache subscription status and tier limits in Redis with a TTL of 5–15 minutes. Validate keys against the cache first, falling back to your database only on cache misses.",[14,1821,1822,1825,1826,1829,1830,1833],{},[20,1823,1824],{},"What's the best way to test Stripe webhooks locally during development?","\nUse the Stripe CLI: ",[69,1827,1828],{},"stripe listen --forward-to localhost:8000\u002Fwebhooks\u002Fstripe",". This tunnels live events to your local FastAPI\u002FFlask server. Always test signature verification against the CLI-provided webhook secret (",[69,1831,1832],{},"whsec_...",") to ensure production parity.",[1835,1836,1837],"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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);}",{"title":83,"searchDepth":101,"depth":101,"links":1839},[1840,1841,1842,1843,1844,1845,1846],{"id":45,"depth":101,"text":46},{"id":63,"depth":101,"text":64},{"id":515,"depth":101,"text":516},{"id":930,"depth":101,"text":931},{"id":1326,"depth":101,"text":1327},{"id":1749,"depth":101,"text":1750},{"id":1792,"depth":101,"text":1793},"md",{},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fhow-to-charge-for-api-access-using-stripe",{"title":5,"description":16},"building-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fhow-to-charge-for-api-access-using-stripe\u002Findex","81y87glk9i6x7zo9pdySFN9ezNNSPUbVR8czrLhuy6M",{"@context":1854,"@type":1855,"mainEntity":1856},"https:\u002F\u002Fschema.org","FAQPage",[1857,1862,1865,1868],{"@type":1858,"name":1798,"acceptedAnswer":1859},"Question",{"@type":1860,"text":1861},"Answer","Stripe's automated dunning management retries failed charges over several days. Implement a grace period in your database logic (e.g., 3–7 days) before revoking keys. Listen for invoice.payment_failed to trigger user notifications via email or dashboard alerts.",{"@type":1858,"name":1808,"acceptedAnswer":1863},{"@type":1860,"text":1864},"Yes. Use stripe.SubscriptionItem.create_usage_record() to report usage at the end of each billing cycle or daily. Pair this with a Redis counter to track requests per API key accurately and report in bulk to minimize API overhead.",{"@type":1858,"name":1818,"acceptedAnswer":1866},{"@type":1860,"text":1867},"Never call the Stripe API synchronously on every request. Cache subscription status and tier limits in Redis with a TTL of 5–15 minutes. Validate keys against the cache first, falling back to your database only on cache misses.",{"@type":1858,"name":1824,"acceptedAnswer":1869},{"@type":1860,"text":1870},"Use the Stripe CLI: stripe listen --forward-to localhost:8000\u002Fwebhooks\u002Fstripe. This tunnels live events to your local FastAPI\u002FFlask server. Always test signature verification against the CLI-provided webhook secret (whsec_...) to ensure production parity.",1778017886203]