Payment processing is the lifeblood of any online business, but what happens after a charge succeeds is just as critical. Updating user accounts, provisioning services, sending receipts, and logging transactions are all essential tasks that can quickly become a complex, brittle web of custom code.
Stripe webhooks provide the real-time event notifications you need, but how do you handle them in a way that is reliable, scalable, and easy to maintain?
Enter the Atomic Action.
On the .do platform, we believe that complex processes are built from simple, powerful building blocks. By treating each step of your post-payment logic as a discrete, self-contained action, you can build incredibly robust agentic workflows. In this post, we'll walk you through integrating Stripe webhooks with .do by creating a reusable action to handle successful payments.
Webhooks are fundamentally event-driven. An event happens, and a notification is sent. An atomic action is the perfect "verb" to respond to that event "noun."
Let's build a practical example. When a charge.succeeded event occurs in Stripe, we want to:
Here’s how you would define this as an atomic action on the .do platform.
import { Action } from '@do-co/agent';
// Assume db and auditLog are your imported services
import { db, auditLog, emailService } from './services';
// Define a new atomic action to handle a successful payment
const handleSuccessfulPayment = new Action('stripe-handle-successful-payment', {
title: 'Handle Successful Stripe Payment',
description: 'Updates a user account after a charge.succeeded event.',
input: {
// We expect the customer and charge IDs from the Stripe event object
customerId: { type: 'string', required: true },
chargeId: { type: 'string', required: true },
},
async handler({ customerId, chargeId }) {
// 1. Find the user in our local database
console.log(`Looking up user for Stripe customer: ${customerId}`);
const user = await db.findUserByStripeId(customerId);
if (!user) {
// Handling edge cases is crucial for robust automation.
// The .do platform can log this failure for review.
throw new Error(`User not found for Stripe customer ID: ${customerId}`);
}
// 2. Update user account (this step is idempotent)
await db.updateUser(user.id, {
subscriptionStatus: 'active',
lastChargeId: chargeId,
});
console.log(`User ${user.id} subscription set to active.`);
// 3. Log the transaction for auditing
await auditLog.create({
userId: user.id,
action: 'payment_succeeded',
details: { chargeId, customerId }
});
// Pro-tip: You can even chain another action!
// await sendPaymentReceiptEmail.run({ to: user.email, chargeId });
return { success: true, userId: user.id };
},
});
// To test or run this action manually:
// const result = await handleSuccessfulPayment.run({
// customerId: 'cus_xxxxxxxxxxxxxx',
// chargeId: 'ch_xxxxxxxxxxxxxx',
// });
// console.log(result);
With our business logic perfectly encapsulated in the handleSuccessfulPayment action, connecting it to the outside world is trivial. You simply need a thin API layer (like an Express route or a serverless function) to receive the webhook, validate it, and execute the action.
// Example using an Express.js server
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), async (req, res) => {
let event;
const signature = req.headers['stripe-signature'];
// 1. CRITICAL: Verify the event came from Stripe
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.log(`⚠️ Webhook signature verification failed.`);
return res.sendStatus(400);
}
// 2. Handle the 'charge.succeeded' event type
if (event.type === 'charge.succeeded') {
const charge = event.data.object;
// 3. Execute our robust, testable, and reusable atomic action
try {
await handleSuccessfulPayment.run({
customerId: charge.customer,
chargeId: charge.id,
});
} catch (error) {
// If the action fails, the .do platform can handle logging,
// alerting, and even retry policies.
console.error('Action execution failed:', error);
return res.status(500).json({ error: 'Internal Server Error' });
}
}
// 4. Acknowledge receipt of the event
res.status(200).json({ received: true });
});
Notice the beautiful separation of concerns. The web server's only job is to handle HTTP communication and security validation. The real work—the core business logic—is delegated to the .do action.
This is where the magic happens. Your handleSuccessfulPayment action is now a fundamental building block. You can use it to construct more powerful, intelligent, and automated agentic workflows.
Consider a "New Premium User Onboarding" workflow:
By composing simple atomic actions, you can orchestrate complex business processes that are easy to visualize, manage, and scale. Stop writing tangled, monolithic webhook handlers. Start building with the atomic unit of work.
Ready to transform your business logic into programmable building blocks? Get started on the .do platform and build your first agentic workflow today.
What is an atomic action in the .do platform?
An atomic action is the smallest, indivisible unit of work in a workflow. It's a self-contained, executable task—like sending an email, querying a database, or calling an external API. Each action is designed to do one thing well.
How are actions different from workflows?
Actions are the individual steps, while workflows are the orchestration of multiple actions in a specific sequence or logic. You build complex workflows by composing simple, reusable actions together.
Can I create my own custom actions?
Absolutely. The .do SDK allows you to define custom actions with specific inputs, outputs, and business logic. This transforms your unique business operations into reusable, programmable building blocks for any workflow.