Deploy a Stripe webhook server to manage post-payment actions with Airtable
Install NPM Packages
npm i -S stripe
npm i -S express
npm i -S cors
npm i -S body-parser
npm i -S node-fetch@2.6.6
npm i -S nodemailer
npm i -S jsonwebtoken
Install and configure the Gmail plugin
(Bottom right button)
Copy the
gmail_app_password
:XXXXXX
Create and save a Token
data.records:read
data.records:write
schema.bases:read
schema.bases:write
Access:
Your workspaceCopy the
airtable_base
:"appXXX"
Copy theairtable_token
:XXXXX
Copy theairtable_table
:tblXXX
Copy the test price id
product1_price
:price_XXXXXX
Copy the test
stripe_key
:sk_test_XXXXX
stripe login
stripe listen --forward-to localhost:4242/webhook
Copy the test
stripe_ws_secret
:whsec_XXXXX
Success payment: Card Number test: 4242 4242 4242 4242 Expiration Date: Use a future date CVC: 000
Error with insufficient_funds
:
Card Number test: 4000 0000 0000 9995
You can also manually trigger an event with a demo response:
stripe trigger checkout.session.completed
Copy the live stripe_key
: sk_live_XXXXX
checkout.session.async_payment_failed
checkout.session.async_payment_succeeded
checkout.session.completed
Copy the live stripe_ws_secret
: whsec_XXXXX
Copy the live product1_price
: price_XXXXX
Credential file credentials-properties.json
{
"stripe_key": "sk_test_XXX",
"stripe_ws_secret": "whsec_XXX",
"url_domain": "http://localhost:4242",
"product1_price": "price_XXXXX",
"gmail_app_password": "XXXXX",
"airtable_base": "appXXXXX",
"airtable_token": "XXXXXXX",
"airtable_table": "tblXXXXXX"
}
{
"stripe_key": "sk_live_XXXXX",
"stripe_ws_secret": "whsec_XXXXX",
"url_domain": "http://worker1.znote.io/XXXXX/",
"product1_price": "price_XXXXX",
"gmail_app_password": "XXXXX",
"airtable_base": "appXXXXX",
"airtable_token": "XXXXXXX",
"airtable_table": "tblXXXXXX"
}
Write credential files above
const credentials = loadBlock('credentials-dev');
_fs.writeFileSync(`${__dirname}/credentials-dev-properties.json`, credentials);
const credentials = loadBlock('credentials-prod');
_fs.writeFileSync(`${__dirname}/credentials-prod-properties.json`, credentials);
Open the Zenv folder and copy your downloadable content
open .
//exec: node
const properties = require("./credentials-dev-properties.json");
const fetch = require('node-fetch');
const stripe = require('stripe')(properties.stripe_key);
const express = require('express');
var jwt = require('jsonwebtoken');
const cors = require('cors')
const bodyParser = require('body-parser');
const nodemailer = require('nodemailer');
const Airtable = require("./airtable-api.js");
// Init Airtable
const BASE = properties.airtable_base;
const TOKEN = properties.airtable_token;
const TABLE = properties.airtable_table;
const airtable = new Airtable(TOKEN, BASE);
const MAX_DOWNLOAD_COUNT = 5; // Download limit
const JWT_SECRET = "CHANGE_ME"; // Secret to sign the link sent to client
const DOWNLOADABLE_FILE = "MY_EBOOK.pdf"; // Content file to download
const app = express();
app.use(cors());
const YOUR_DOMAIN = properties.url_domain;
// Your endpoint secret from your Stripe dashboard webhook settings
const endpointSecret = properties.stripe_ws_secret;
app.get('/', async (request, response) => {
response.sendStatus(200);
});
app.get('/download/*', async (request, response) => {
try {
const token = request.params[0].split("/")[0];
const decoded = jwt.verify(token, JWT_SECRET);
const email = decoded.email;
console.log(email);
const result = await airtable.searchRecord(TABLE, 'SEARCH(SUBSTITUTE("'+email+'","+","%2B"), {email})');
if (result.records.length !== 0) {
const userId = result.records[0].id;
const nbrOfDownload = result.records[0].fields.nbrOfDownload;
if (nbrOfDownload <= MAX_DOWNLOAD_COUNT) {
// Increment the download count for the user
let userDownloads = nbrOfDownload + 1;
const data = {
"fields": {
"nbrOfDownload": userDownloads
}
};
const result = await airtable.updateRecord(TABLE, userId, data);
printJSON(result);
// Return downloadable content
const file = `./${DOWNLOADABLE_FILE}`;
return response.download(file);
}
}
return response.status(403).send("You couldn't download this content");
} catch(err) {
console.log(err);
return response.sendStatus(500);
}
});
/**
* Process after payment: Generate license
*/
app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (request, response) => {
const payload = request.body;
const sig = request.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(payload, sig, endpointSecret);
} catch (err) {
return response.status(400).send(`Webhook Error: ${err.message}`);
}
const session = event.data.object;
console.log(event.type);
switch (event.type) {
case 'checkout.session.completed':
// Fulfill order
const creationDate = new Date().toISOString().split('T')[0];
const product = session.metadata.product;
const email = session.customer_details.email;
console.log(product);
console.log(email);
// Compute link
const token = jwt.sign({ email: email, product: product }, JWT_SECRET);
const link = YOUR_DOMAIN + "/download/" + token;
// Create customer record in Airtable
try {
const data = {
"records": [
{
"fields": {
"Email": email,
"purchaseDate": creationDate,
"nbrOfDownload": 0,
"version": 1
}
}
]
};
const result = await airtable.createRecord(TABLE, data);
printJSON(result);
} catch(err) {
print(err); // Log Airtable error if any
}
// Send Thank you email with download link
const sender = "lagrede.anthony@gmail.com";
const recipient = email;
const subject = "Thank you for your purchase 😊";
const content = `
Hi 👋, <br/>Thank you for your purchase!<br/><br/>
Here is your link to download your content: ${link} <br/><br/>
You have ${MAX_DOWNLOAD_COUNT} downloads left.<br/><br/>
Best regards<br/>
The Znote Team
`;
sendMailWithGmail(sender, recipient, subject, content);
print("✅ Email sent successfully!");
break;
default:
// Unhandled event type
}
response.sendStatus(200);
});
app.listen(4242, () => console.log('Running on port 4242'));