├── .gitignore ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fireapi3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.17.1", 14 | "stripe": "^8.184.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## API Monetization Demo with Stripe Metered Billing 2 | 3 | Learn how to [make money from an API](https://youtu.be/MbqSMgMAzxU) on YouTube. 4 | 5 | Detailed breakdown of the [API monetization](https://fireship.io/lessons/api-monetization-stripe) code Fireship. 6 | 7 | ## Run it 8 | 9 | - Create a metered billing product in Stripe 10 | - Install Stripe CLI 11 | - Add your testing Stripe secret key in the index.js 12 | 13 | ```bash 14 | git clone 15 | npm i 16 | node . 17 | stripe listen --forward-to localhost:8080/webhook 18 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | 4 | const stripe = require('stripe')('sk_test_YOUR-KEY'); 5 | 6 | // Middleware required for Webhook Handler 7 | app.use( 8 | express.json({ 9 | verify: (req, res, buffer) => (req['rawBody'] = buffer), 10 | }) 11 | ); 12 | 13 | ////// Data Model /////// 14 | 15 | // TODO Implement a real database 16 | // Reverse mapping of stripe to API key. Model this in your preferred database. 17 | const customers = { 18 | // stripeCustomerId : data 19 | 'stripeCustomerId': { 20 | apiKey: '123xyz', 21 | active: false, 22 | itemId: 'stripeSubscriptionItemId', 23 | }, 24 | }; 25 | const apiKeys = { 26 | // apiKey : customerdata 27 | '123xyz': 'stripeCustomerId', 28 | }; 29 | 30 | ////// Custom API Key Generation & Hashing /////// 31 | 32 | // Recursive function to generate a unique random string as API key 33 | function generateAPIKey() { 34 | const { randomBytes } = require('crypto'); 35 | const apiKey = randomBytes(16).toString('hex'); 36 | const hashedAPIKey = hashAPIKey(apiKey); 37 | 38 | // Ensure API key is unique 39 | if (apiKeys[hashedAPIKey]) { 40 | generateAPIKey(); 41 | } else { 42 | return { hashedAPIKey, apiKey }; 43 | } 44 | } 45 | 46 | // Hash the API key 47 | function hashAPIKey(apiKey) { 48 | const { createHash } = require('crypto'); 49 | 50 | const hashedAPIKey = createHash('sha256').update(apiKey).digest('hex'); 51 | 52 | return hashedAPIKey; 53 | } 54 | 55 | ////// Express API /////// 56 | 57 | // Create a Stripe Checkout Session to create a customer and subscribe them to a plan 58 | app.post('/checkout', async (req, res) => { 59 | const session = await stripe.checkout.sessions.create({ 60 | mode: 'subscription', 61 | payment_method_types: ['card'], 62 | line_items: [ 63 | { 64 | price: 'price_YOUR-PRODUCT', 65 | }, 66 | ], 67 | // {CHECKOUT_SESSION_ID} is a string literal; do not change it! 68 | // the actual Session ID is returned in the query parameter when your customer 69 | // is redirected to the success page. 70 | success_url: 71 | 'http://YOUR-WEBSITE/dashboard?session_id={CHECKOUT_SESSION_ID}', 72 | cancel_url: 'http://YOUR-WEBSITE/error', 73 | }); 74 | 75 | res.send(session); 76 | }); 77 | 78 | // Listen to webhooks from Stripe when important events happen 79 | app.post('/webhook', async (req, res) => { 80 | let data; 81 | let eventType; 82 | // Check if webhook signing is configured. 83 | const webhookSecret = 'whsec_YOUR-KEY'; 84 | 85 | if (webhookSecret) { 86 | // Retrieve the event by verifying the signature using the raw body and secret. 87 | let event; 88 | let signature = req.headers['stripe-signature']; 89 | 90 | try { 91 | event = stripe.webhooks.constructEvent( 92 | req['rawBody'], 93 | signature, 94 | webhookSecret 95 | ); 96 | } catch (err) { 97 | console.log(`⚠️ Webhook signature verification failed.`); 98 | return res.sendStatus(400); 99 | } 100 | // Extract the object from the event. 101 | data = event.data; 102 | eventType = event.type; 103 | } else { 104 | // Webhook signing is recommended, but if the secret is not configured in `config.js`, 105 | // retrieve the event data directly from the request body. 106 | data = req.body.data; 107 | eventType = req.body.type; 108 | } 109 | 110 | switch (eventType) { 111 | case 'checkout.session.completed': 112 | console.log(data); 113 | // Data included in the event object: 114 | const customerId = data.object.customer; 115 | const subscriptionId = data.object.subscription; 116 | 117 | console.log( 118 | `💰 Customer ${customerId} subscribed to plan ${subscriptionId}` 119 | ); 120 | 121 | // Get the subscription. The first item is the plan the user subscribed to. 122 | const subscription = await stripe.subscriptions.retrieve(subscriptionId); 123 | const itemId = subscription.items.data[0].id; 124 | 125 | // Generate API key 126 | const { apiKey, hashedAPIKey } = generateAPIKey(); 127 | console.log(`User's API Key: ${apiKey}`); 128 | console.log(`Hashed API Key: ${hashedAPIKey}`); 129 | 130 | // Store the API key in your database. 131 | customers[customerId] = { 132 | apikey: hashedAPIKey, 133 | itemId, 134 | active: true, 135 | }; 136 | apiKeys[hashedAPIKey] = customerId; 137 | 138 | break; 139 | case 'invoice.paid': 140 | // Continue to provision the subscription as payments continue to be made. 141 | // Store the status in your database and check when a user accesses your service. 142 | // This approach helps you avoid hitting rate limits. 143 | break; 144 | case 'invoice.payment_failed': 145 | // The payment failed or the customer does not have a valid payment method. 146 | // The subscription becomes past_due. Notify your customer and send them to the 147 | // customer portal to update their payment information. 148 | break; 149 | default: 150 | // Unhandled event type 151 | } 152 | 153 | res.sendStatus(200); 154 | }); 155 | 156 | // Get information about the customer 157 | app.get('/customers/:id', (req, res) => { 158 | const customerId = req.params.id; 159 | const account = customers[customerId]; 160 | if (account) { 161 | res.send(account); 162 | } else { 163 | res.sendStatus(404); 164 | } 165 | }); 166 | 167 | // Make a call to the API 168 | app.get('/api', async (req, res) => { 169 | const { apiKey } = req.query; 170 | // const apiKey = req.headers['X-API-KEY'] // better option for storing API keys 171 | 172 | if (!apiKey) { 173 | res.sendStatus(400); // bad request 174 | } 175 | 176 | const hashedAPIKey = hashAPIKey(apiKey); 177 | 178 | const customerId = apiKeys[hashedAPIKey]; 179 | const customer = customers[customerId]; 180 | 181 | if (!customer || !customer.active) { 182 | res.sendStatus(403); // not authorized 183 | } else { 184 | 185 | // Record usage with Stripe Billing 186 | const record = await stripe.subscriptionItems.createUsageRecord( 187 | customer.itemId, 188 | { 189 | quantity: 1, 190 | timestamp: 'now', 191 | action: 'increment', 192 | } 193 | ); 194 | res.send({ data: '🔥🔥🔥🔥🔥🔥🔥🔥', usage: record }); 195 | } 196 | }); 197 | 198 | app.get('/usage/:customer', async (req, res) => { 199 | const customerId = req.params.customer; 200 | const invoice = await stripe.invoices.retrieveUpcoming({ 201 | customer: customerId, 202 | }); 203 | 204 | res.send(invoice); 205 | }); 206 | 207 | app.listen(8080, () => console.log('alive on http://localhost:8080')); 208 | --------------------------------------------------------------------------------