6 min read

How to Track Payment Intents in Stripe

Payment Intents are the backbone of Stripe's modern payment flow, but they're opaque by default. You create one, pass it to your frontend, and then what? Without proper tracking, you'll lose visibility into payment status, failed transactions, and incomplete checkouts. Here's how to set up tracking that actually works.

Create and Track Payment Intents with Webhooks

Webhooks are the most reliable way to track Payment Intent status changes in real-time. Every status transition—from requires_payment_method to succeeded—fires an event you can listen to.

Create a Payment Intent on your backend

When a customer initiates checkout, create a Payment Intent in your backend. Pass the client_secret to your frontend so it can confirm the payment.

javascript
const stripe = require('stripe')('sk_live_...');

// In your backend API endpoint
const paymentIntent = await stripe.paymentIntents.create({
  amount: 2000, // $20.00 in cents
  currency: 'usd',
  metadata: {
    user_id: '12345',
    order_id: 'order_abc'
  }
});

// Send client_secret to frontend
res.json({ clientSecret: paymentIntent.client_secret });
Create a Payment Intent with metadata to link it back to your order system

Listen for payment_intent.succeeded webhook events

Enable the payment_intent.succeeded event in your Stripe Dashboard under Webhooks > Endpoints. This fires when a payment actually succeeds, giving you a reliable signal to fulfill the order.

javascript
const express = require('express');
const app = express();

app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'payment_intent.succeeded') {
    const paymentIntent = event.data.object;
    console.log(`Payment succeeded: ${paymentIntent.id}`);
    fulfillOrder(paymentIntent.metadata.order_id);
  }

  res.json({received: true});
});
Handle payment_intent.succeeded on your webhook endpoint

Track failed payments with payment_intent.payment_failed

Listen for payment_intent.payment_failed to catch declined transactions. This includes soft declines (insufficient funds) that customers might retry.

javascript
if (event.type === 'payment_intent.payment_failed') {
  const paymentIntent = event.data.object;
  
  console.error(`Payment failed: ${paymentIntent.id}`, {
    status: paymentIntent.status,
    last_error: paymentIntent.last_payment_error,
    amount: paymentIntent.amount
  });
  
  sendEmail(paymentIntent.metadata.user_email, {
    subject: 'Payment Failed',
    message: `We couldn't process your payment. Error: ${paymentIntent.last_payment_error.message}`
  });
}
Detect failures and notify customers immediately
Tip: Enable multiple webhook events—not just payment_intent.succeeded. Listen for payment_intent.amount_capturable_updated, payment_intent.canceled, and charge.refunded to catch edge cases.

Query Payment Intents Programmatically

Webhooks give you real-time updates, but sometimes you need to pull Payment Intent data on-demand. Use Stripe's search and retrieve APIs to check status, filter by metadata, or debug incomplete transactions.

Retrieve a specific Payment Intent by ID

If you know the Payment Intent ID (e.g., from your order database), fetch it directly to check its current status without waiting for webhooks.

javascript
const paymentIntent = await stripe.paymentIntents.retrieve('pi_1A1A1A1A1A1A1A1A');

console.log({
  id: paymentIntent.id,
  status: paymentIntent.status, // 'succeeded', 'requires_action', etc.
  amount: paymentIntent.amount,
  currency: paymentIntent.currency,
  created: new Date(paymentIntent.created * 1000)
});
Check the status of a Payment Intent you already know the ID of

List Payment Intents with filters and pagination

Fetch multiple Payment Intents by status, date range, or customer. Use limit and starting_after for pagination when scanning large datasets.

javascript
// Get all succeeded payments from the last 7 days
const sevenDaysAgo = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60;

const intents = await stripe.paymentIntents.list({
  status: 'succeeded',
  created: { gte: sevenDaysAgo },
  limit: 100,
  expand: ['data.customer']
});

intents.data.forEach(intent => {
  console.log(`${intent.id}: $${(intent.amount / 100).toFixed(2)} - ${intent.status}`);
});
List succeeded payments from the last week with customer details expanded

Search Payment Intents using the Search API

Use the Search API for complex queries across Payment Intents. Search by metadata, customer ID, amount, or combine multiple filters with AND/OR logic.

javascript
// Find all failed payment intents for a specific user
const failedIntents = await stripe.paymentIntents.search({
  query: "status:'requires_payment_method' AND metadata['user_id']:'12345'",
  limit: 50
});

console.log(`Found ${failedIntents.data.length} payment intents that need attention`);

failedIntents.data.forEach(intent => {
  console.log({
    id: intent.id,
    customer: intent.customer,
    amount: intent.amount,
    status: intent.status
  });
});
Search across Payment Intents with complex filters using the Search API
Watch out: The Search API has a 30-second timeout. If you're querying millions of Payment Intents, break it into smaller date ranges or use the List API with pagination.

Build Real-Time Payment Tracking

Once you're capturing Payment Intent events, log them to your database and build aggregations. This gives you dashboards, alerts, and the ability to debug individual transactions.

Log payment events to your database

Store each webhook event (succeeded, failed, requires_action) in your database with relevant metadata. This becomes your source of truth for payment history and metrics.

javascript
// Log every payment_intent event to your database
const logPaymentEvent = async (event) => {
  const intent = event.data.object;
  await db.query(`
    INSERT INTO payment_events (event_id, payment_intent_id, status, amount, timestamp, user_id)
    VALUES ($1, $2, $3, $4, $5, $6)
    ON CONFLICT (event_id) DO NOTHING
  `, [event.id, intent.id, event.type.split('.')[1], intent.amount, new Date(event.created * 1000), intent.metadata.user_id]);
};

// Then aggregate by hour for your dashboard
const hourlyMetrics = await db.query(`
  SELECT 
    DATE_TRUNC('hour', timestamp) AS hour,
    COUNT(*) as total,
    COUNT(*) FILTER (WHERE status = 'succeeded') as succeeded,
    COUNT(*) FILTER (WHERE status = 'payment_failed') as failed,
    SUM(amount) FILTER (WHERE status = 'succeeded') as revenue_cents
  FROM payment_events
  GROUP BY hour
  ORDER BY hour DESC
  LIMIT 24
`);

return hourlyMetrics;
Log webhook events idempotently and compute success rate by hour
Tip: Design webhook handlers to be idempotent. Stripe may retry events if your endpoint times out. If you insert with ON CONFLICT DO NOTHING, duplicate events won't break your data.

Common Pitfalls

  • Relying on succeeded webhooks as synchronous. They can be delayed by seconds. Design your backend to be idempotent—handle the same webhook multiple times without double-charging or double-fulfilling orders.
  • Ignoring payment_intent.requires_action. Payments requiring 3D Secure or other customer authentication trigger this status. If you only listen for succeeded, customers get stuck and abandon checkout.
  • Querying too aggressively from the frontend. Don't poll payment intents every 500ms. Use webhooks or a polling interval of at least 5-10 seconds if you absolutely must poll.
  • Storing client_secret in your database. The client_secret should only exist in memory on the frontend or be passed directly from your backend. If compromised, attackers can confirm payments.

Wrapping Up

Payment Intents are powerful, but you need proper tracking to use them well. Webhooks give you real-time visibility; search and retrieve APIs give you historical queries and debugging. Start with webhooks for production, then layer in dashboards and queries as your payment volume grows. If you want to track Payment Intent metrics alongside customer engagement and product usage across all your tools, Product Analyst can help.

Track these metrics automatically

Product Analyst connects to your stack and surfaces the insights that matter.

Try Product Analyst — Free