One of our users came to us with an unusual request:
“Can I make my card only work on the second attempt for certain purchases at night?”
It turns out, the idea had a clever purpose. The user described themselves as a compulsive late-night shopper, a few glasses of wine and some social scrolling often led to impulsive online purchases. They wanted a built-in pause, a kind of digital speed bump, to make sure they truly wanted the item before the transaction went through.
So we decided to help them by creating a quick little script that will make the card only work when tried twice from 8pm to 7am on online purchases.
Our strategy will be creating a “Time of Day” transaction rule with a filter for the card that filters only “e-commerce” transaction. We will then use Klutch’s webhook to listen to declines based on that rule and override the rule once triggered so that the retry works.
Note: The retry logic can also be done by using Klutch’s automatic SMS Text override for user-rules, but we will do that using the webhook for educational purposes.
Let's set up our project:
This article assumes that you have the following setup:
Also, for article’s sake we are going to assume that all income is automatically loaded into your Klutch card (in this case we are using the Klutch Spend Card) by using Klutch’s “Transfer from your bank” feature.
Let’s start with a simple Node.js template to call our APIs and authenticate with the Klutch API.
We use “createSessionToken” to create a token for this session.
import dotenv from 'dotenv';
dotenv.config();
const ENDPOINT = process.env.KLUTCH_ENDPOINT;
const CLIENT_ID = process.env.KLUTCH_CLIENT_ID;
const SECRET_KEY = process.env.KLUTCH_SECRET_KEY;
const CARD_ID = process.env.CARD_ID;
async function gql(query, variables, token) {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: {
"content-type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {})
},
body: JSON.stringify({ query, variables })
});
const json = await res.json();
if (json.errors) throw new Error(JSON.stringify(json.errors));
return json.data;
}
async function main() {
const token = await getToken()
}
async function getToken() {
const { createSessionToken } = await gql(
`mutation($clientId:String,$secretKey:String){
createSessionToken(clientId:$clientId, secretKey:$secretKey)
}`,
{ clientId: CLIENT_ID, secretKey: SECRET_KEY }
);
const token = createSessionToken;
return token
}
main().catch(err => {
console.error("Error", err.message);
process.exit(1);
});
Let’s create our time of day rule. Replace CARD_ID with the card Id for the card you want to create the rule to.
Once that rule is created, transactions made from 8pm until 7pm next day that are made without the card present will be declined.
async function createTransactionRule(token) {
const queryVar = {
name: `swipe_twice_rule`,
displayName: `Swipe Twice`,
cards: [CARD_ID],
spec: {
specType: "TimeOfDayTransactionRule",
startTime: "20:00:00",
endTime: "07:00:00",
filters: [
{
field: "CARD_PRESENT",
operator: "EQUALS",
value: '"CARD_NOT_PRESENT"'
}
]
}
}
const {createTransactionRule} = await gql(`
mutation($name: String, $displayName: String, $cardIds: [String], $spec: TransactionRuleSpecInput) {
createTransactionRule(name: $name, displayName: $displayName, cardIds: $cardIds, spec: $spec) {
id
name
}
}`, queryVar, token)
return createTransactionRule
}
We add that to our main function:
async function main() {
const token = await getToken()
createTransactionRule(token)
}
For this project, we will also need to listen to Klutch’s webhook. To configure the webhook, please visit “My Account” -> “Developers”
So, let’s set that up. We are using Express as our webserver for the webhook
import express from "express";
const app = express();
app.use(
express.raw({ type: "application/json" })
);
app.post("/webhook/klutch", (req, res) => {
try {
const payload = JSON.parse(req.body.toString("utf-8"));
const eventType = payload.event;
const data = payload.data;
res.status(200).send("ok");
} catch (err) {
console.error("Error handling webhook:", err);
res.status(500).send("Server error");
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
Klutch’s webhook message follows the following format:
{
"principal": {
"_alloyCardType": "com.alloycard.core.entities.user.User",
"entityID": "<<USERID>>"
},
"event": {
"_alloyCardType": "com.alloycard.core.entities.transaction.TransactionCreatedEvent",
"transaction": {
"_alloyCardType": "com.alloycard.core.entities.transaction.Transaction",
"entityID": "txn_ed07bcaedc374d5dbadef4a229cd327d"
},
"createdAt": 1636006199,
"eventId": "ad2e1f37-2f36-4978-bd85-2220776064bb"
},
"webhookUrl": ""
}
Let’s create code that reads the event webhook, make sure that it is a “TransactionCreatedEvent” and then fetch the transaction from Klutch’s server.
app.post("/webhook/klutch", (req, res) => {
try {
const payload = JSON.parse(req.body.toString("utf-8"));
// Check if this is a transaction created event
if (payload.event?._alloyCardType !== "com.alloycard.core.entities.transaction.TransactionCreatedEvent") {
console.log("Ignoring non-transaction event:", payload.event?._alloyCardType);
return res.status(200).send("ok");
}
const transactionId = payload.event.transaction.entityID;
// Process transaction asynchronously to not block webhook response
handleTransaction(transactionId).catch(err => {
console.error("Error processing transaction:", transactionId, err);
});
res.status(200).send("ok");
} catch (err) {
console.error("Error handling webhook:", err);
res.status(500).send("Server error");
}
});
We also need to implement our handleTransaction function. That function will check if the transaction is made with our Card and if it’s declined because of our transaction rule, if so, we will override the rule to allow it to go through on the second time.
async function handleTransaction(transactionId) {
const token = await getToken()
// Fetch transaction details
const { transaction } = await gql(`
query($id: String!) {
transaction(id: $id) {
id
amount
transactionStatus
declineReason
cardPresent
}
}
`, { id: transactionId }, token);
if (transaction.transactionStatus == "DECLINED" && transaction.declineReason.includes("Swipe Twice")) {
await temorarilyDisableRule(token, 5 * 60)
}
console.log("Processed transaction:", transaction);
return transaction;
}
async function temorarilyDisableRule(token, duration) {
const {transactionRule} = await gql(`
mutation($name: String, $duration: Int) {
transactionRule(name: $name) {
disableFor(durationInSeconds: $duration) {
id
}
}
}
`, {name: "swipe_twice_rule", duration}, token )
return transactionRule
}
Now every time that transaction gets declined, our code will enable it on the second attempt.
This concludes this tutorial, but this is just a small sample of what can be accomplished with Klutch.
As a next step, think about enhancing this code by specifying even more conditions where transactions need to be declined or approved.
You can check out the entire source code for this tutorial on Github
Klutch grew from our belief that people should demand more from technology. So when we noticed a critical need for more control and flexibility in how people make payments, we went to work pioneering a framework that delivered just that.
