stripe-terminal-issuing

zacharyr0th's avatarfrom zacharyr0th

Use when implementing in-person payments with Terminal or issuing cards with Stripe Issuing. Invoke for POS terminal setup, card reader integration, in-person payment processing, card issuance, authorization controls, or physical/virtual card management.

⭐1stars🔀0forks📁View on GitHub🕐Updated Nov 7, 2025

When & Why to Use This Skill

This Claude skill provides expert technical guidance for integrating Stripe Terminal and Stripe Issuing into your applications. It simplifies the complexity of physical payment hardware setup, card reader SDK integration, and the creation of custom card programs with robust authorization controls and spending limits.

Use Cases

  • Setting up and configuring physical POS card readers (M2, BBPOS, Verifone) for retail and in-person payment environments.
  • Developing custom card programs to issue instant virtual cards or physical cards for employee expenses and fintech platforms.
  • Implementing real-time authorization webhooks to programmatically approve or decline transactions based on custom business logic.
  • Managing the end-to-end card lifecycle, including setting spending limits, merchant category blocking, and handling card disputes.
  • Debugging connection tokens and SDK integration issues across JavaScript, iOS, and Android platforms for seamless payment collection.
namestripe-terminal-issuing
descriptionUse when implementing in-person payments with Terminal or issuing cards with Stripe Issuing. Invoke for POS terminal setup, card reader integration, in-person payment processing, card issuance, authorization controls, or physical/virtual card management.
allowed-toolsRead, Grep, Glob

Stripe Terminal & Issuing Expert

Purpose

Expert knowledge of Stripe Terminal for in-person payments and Stripe Issuing for card programs. Covers POS reader setup, Terminal SDK integration, card issuance, authorization controls, and card lifecycle management.

When to Use

Invoke this skill when:

  • Implementing in-person payments
  • Setting up card readers (Terminal)
  • Processing payments at physical locations
  • Creating card programs (Issuing)
  • Issuing physical or virtual cards
  • Setting up spending controls
  • Managing card authorizations
  • Handling card disputes
  • Debugging Terminal or Issuing issues

Documentation Available

Location: /Users/zach/Documents/cc-skills/docs/stripe/

Coverage (~149 files in terminal/ + issuing/):

  • Terminal (~75 files):

    • Reader management (Stripe Reader M2, BBPOS, Verifone)
    • Connection tokens
    • Payment collection
    • Refunds and cancellations
    • Terminal SDK (JS, iOS, Android)
    • Reader locations
  • Issuing (~74 files):

    • Card creation (physical, virtual)
    • Cardholder management
    • Authorization controls
    • Spending limits
    • Authorization holds
    • Card disputes
    • Transaction management
  • stripe-payments: For online payment flows
  • stripe-api-integration: For webhooks and API patterns

Best Practices

Terminal

  • Test with simulated readers - Use test mode readers
  • Handle connection states - Manage reader connections properly
  • Implement offline mode - Store payments when offline
  • Update reader software - Keep firmware updated
  • Handle timeouts - Set appropriate payment timeouts

Issuing

  • Set authorization controls - Prevent fraud
  • Monitor spending - Track card usage
  • Handle real-time authorizations - Approve/decline instantly
  • Implement 3DS - For online card use
  • Manage card lifecycle - Issue, activate, cancel properly

Examples

Example 1: Terminal Reader Setup

Question: "How do I set up Stripe Terminal?"

Response:

import { loadStripeTerminal } from '@stripe/terminal-js';

// Backend: Create connection token
export async function createConnectionToken(req, res) {
  try {
    const terminal = require('stripe')(process.env.STRIPE_SECRET_KEY!).terminal;

    const connectionToken = await terminal.connectionTokens.create();

    res.json({ secret: connectionToken.secret });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Frontend: Initialize Terminal SDK
async function initializeTerminal() {
  const StripeTerminal = await loadStripeTerminal();

  const terminal = StripeTerminal.create({
    onFetchConnectionToken: async () => {
      const response = await fetch('/api/connection-token', {
        method: 'POST',
      }).then(r => r.json());

      return response.secret;
    },
    onUnexpectedReaderDisconnect: () => {
      console.log('Reader disconnected unexpectedly');
    },
  });

  return terminal;
}

// Discover readers
async function discoverReaders(terminal) {
  const discoverResult = await terminal.discoverReaders({
    simulated: false, // Set to true for testing
    location: 'tml_xxxxx', // Optional: filter by location
  });

  if (discoverResult.error) {
    console.error('Discovery failed:', discoverResult.error);
  } else {
    console.log('Discovered readers:', discoverResult.discoveredReaders);
    return discoverResult.discoveredReaders;
  }
}

// Connect to reader
async function connectToReader(terminal, reader) {
  const connectResult = await terminal.connectReader(reader);

  if (connectResult.error) {
    console.error('Connect failed:', connectResult.error);
  } else {
    console.log('Connected to reader:', connectResult.reader);
    return connectResult.reader;
  }
}

// Complete Terminal setup
async function setupTerminal() {
  const terminal = await initializeTerminal();
  const readers = await discoverReaders(terminal);

  if (readers && readers.length > 0) {
    await connectToReader(terminal, readers[0]);
  }

  return terminal;
}

References:

  • See: docs/stripe/terminal/

Example 2: Collect Payment with Terminal

Question: "How do I collect an in-person payment?"

Response:

// Backend: Create PaymentIntent for Terminal
export async function createTerminalPaymentIntent(req, res) {
  try {
    const { amount } = req.body;

    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount * 100,
      currency: 'usd',
      payment_method_types: ['card_present'],
      capture_method: 'automatic', // or 'manual' for auth only
      metadata: {
        orderId: 'order_123',
      },
    });

    res.json({ clientSecret: paymentIntent.client_secret });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Frontend: Collect payment
async function collectPayment(terminal: any, amount: number) {
  try {
    // Create PaymentIntent
    const response = await fetch('/api/create-terminal-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ amount }),
    }).then(r => r.json());

    // Collect payment method
    const collectResult = await terminal.collectPaymentMethod(
      response.clientSecret
    );

    if (collectResult.error) {
      throw new Error(collectResult.error.message);
    }

    console.log('Payment method collected:', collectResult.paymentIntent);

    // Process payment
    const processResult = await terminal.processPayment(
      collectResult.paymentIntent
    );

    if (processResult.error) {
      throw new Error(processResult.error.message);
    }

    console.log('Payment successful:', processResult.paymentIntent);

    // Optionally confirm on backend
    await fetch('/api/capture-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        paymentIntentId: processResult.paymentIntent.id,
      }),
    });

    return processResult.paymentIntent;
  } catch (error) {
    console.error('Payment failed:', error);
    throw error;
  }
}

// Cancel payment collection
async function cancelPayment(terminal: any) {
  const cancelResult = await terminal.cancelCollectPaymentMethod();

  if (cancelResult.error) {
    console.error('Cancel failed:', cancelResult.error);
  } else {
    console.log('Payment collection canceled');
  }
}

// Handle reader events
terminal.on('connection_status', (status) => {
  console.log('Connection status:', status);
});

terminal.on('payment_status', (status) => {
  console.log('Payment status:', status);
});

References:

  • See: docs/stripe/terminal/payments/

Example 3: Create Location and Register Reader

Question: "How do I register a physical reader?"

Response:

// Create location
export async function createLocation(req, res) {
  try {
    const { displayName, address } = req.body;

    const location = await stripe.terminal.locations.create({
      display_name: displayName,
      address: {
        line1: address.line1,
        city: address.city,
        state: address.state,
        postal_code: address.postalCode,
        country: address.country,
      },
    });

    res.json({ location });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// List locations
export async function listLocations(req, res) {
  try {
    const locations = await stripe.terminal.locations.list({
      limit: 10,
    });

    res.json({ locations: locations.data });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Register reader (done via Stripe Dashboard or mobile app)
// Readers are automatically registered when first connected

// List readers
export async function listReaders(req, res) {
  try {
    const { locationId } = req.query;

    const readers = await stripe.terminal.readers.list({
      location: locationId as string,
      limit: 10,
    });

    res.json({ readers: readers.data });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Update reader
export async function updateReader(req, res) {
  try {
    const { readerId, label, metadata } = req.body;

    const reader = await stripe.terminal.readers.update(readerId, {
      label,
      metadata,
    });

    res.json({ reader });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Delete reader
export async function deleteReader(req, res) {
  try {
    const { readerId } = req.body;

    const deleted = await stripe.terminal.readers.del(readerId);

    res.json({ deleted });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

References:

  • See: docs/stripe/terminal/locations/
  • See: docs/stripe/terminal/readers/

Example 4: Issue Cards

Question: "How do I issue cards with Stripe Issuing?"

Response:

// Create cardholder
export async function createCardholder(req, res) {
  try {
    const { name, email, phone, address } = req.body;

    const cardholder = await stripe.issuing.cardholders.create({
      name,
      email,
      phone_number: phone,
      billing: {
        address: {
          line1: address.line1,
          city: address.city,
          state: address.state,
          postal_code: address.postalCode,
          country: address.country,
        },
      },
      type: 'individual', // or 'company'
      metadata: {
        userId: req.user.id,
      },
    });

    res.json({ cardholder });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Issue virtual card
export async function issueVirtualCard(req, res) {
  try {
    const { cardholderId } = req.body;

    const card = await stripe.issuing.cards.create({
      cardholder: cardholderId,
      currency: 'usd',
      type: 'virtual',
      status: 'active',
      spending_controls: {
        spending_limits: [
          {
            amount: 50000, // $500 per month
            interval: 'per_month',
          },
        ],
        allowed_categories: ['restaurants', 'gas_stations'],
      },
      metadata: {
        purpose: 'Employee expenses',
      },
    });

    res.json({ card });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Issue physical card
export async function issuePhysicalCard(req, res) {
  try {
    const { cardholderId, shippingAddress } = req.body;

    const card = await stripe.issuing.cards.create({
      cardholder: cardholderId,
      currency: 'usd',
      type: 'physical',
      status: 'inactive', // Activate after cardholder receives it
      shipping: {
        name: 'John Doe',
        address: {
          line1: shippingAddress.line1,
          city: shippingAddress.city,
          state: shippingAddress.state,
          postal_code: shippingAddress.postalCode,
          country: shippingAddress.country,
        },
        service: 'standard', // or 'express', 'priority'
      },
    });

    res.json({ card });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Get card details (including sensitive data)
export async function getCardDetails(req, res) {
  try {
    const { cardId } = req.query;

    const card = await stripe.issuing.cards.retrieve(cardId as string, {
      expand: ['number', 'cvc'], // Get full card number and CVC
    });

    res.json({
      number: card.number,
      exp_month: card.exp_month,
      exp_year: card.exp_year,
      cvc: card.cvc,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Update card status
export async function updateCardStatus(req, res) {
  try {
    const { cardId, status } = req.body;
    // status: 'active', 'inactive', 'canceled'

    const card = await stripe.issuing.cards.update(cardId, {
      status,
    });

    res.json({ card });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

Card Types:

  • Virtual: Instant issuance, online use
  • Physical: 3-7 day delivery, in-person use

References:

  • See: docs/stripe/issuing/cards/

Example 5: Authorization Controls

Question: "How do I control card authorizations?"

Response:

// Set spending controls on card
export async function setSpendingControls(req, res) {
  try {
    const { cardId, limits } = req.body;

    const card = await stripe.issuing.cards.update(cardId, {
      spending_controls: {
        spending_limits: [
          {
            amount: 100000, // $1000
            interval: 'per_month',
            categories: ['restaurants'],
          },
          {
            amount: 5000, // $50
            interval: 'per_transaction',
          },
        ],
        allowed_categories: [
          'restaurants',
          'gas_stations',
          'grocery_stores',
        ],
        blocked_categories: [
          'gambling',
          'adult_digital_content',
        ],
      },
    });

    res.json({ card });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Real-time authorization webhook
export async function handleAuthorizationWebhook(req, res) {
  const event = req.body;

  if (event.type === 'issuing_authorization.request') {
    const authorization = event.data.object;

    // Custom authorization logic
    const shouldApprove = await checkAuthorizationRules(authorization);

    if (shouldApprove) {
      // Approve authorization
      await stripe.issuing.authorizations.approve(authorization.id);
    } else {
      // Decline authorization
      await stripe.issuing.authorizations.decline(authorization.id, {
        reason: 'insufficient_funds', // or 'spending_controls'
      });
    }
  }

  res.json({ received: true });
}

async function checkAuthorizationRules(authorization: any): Promise<boolean> {
  // Custom business logic
  const cardholder = await db.cardholders.findUnique({
    where: { stripeId: authorization.cardholder },
  });

  // Check balance
  if (cardholder.balance < authorization.amount) {
    return false;
  }

  // Check merchant category
  if (BLOCKED_MCCS.includes(authorization.merchant_data.category)) {
    return false;
  }

  // Check time of day
  const hour = new Date().getHours();
  if (hour < 6 || hour > 22) {
    return false; // Block late-night transactions
  }

  return true;
}

// List authorizations
export async function listAuthorizations(req, res) {
  try {
    const { cardId } = req.query;

    const authorizations = await stripe.issuing.authorizations.list({
      card: cardId as string,
      limit: 10,
    });

    res.json({ authorizations: authorizations.data });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Capture authorization
export async function captureAuthorization(req, res) {
  try {
    const { authorizationId, captureAmount } = req.body;

    const authorization = await stripe.issuing.authorizations.update(
      authorizationId,
      {
        metadata: { capture_amount: captureAmount },
      }
    );

    res.json({ authorization });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

Authorization States:

  • pending: Awaiting approval
  • approved: Approved
  • declined: Declined

References:

  • See: docs/stripe/issuing/controls/

Common Patterns

Terminal Payment Flow

// 1. Create PaymentIntent (backend)
// 2. Collect payment method (Terminal SDK)
// 3. Process payment (Terminal SDK)
// 4. Confirm payment (webhook)

Card Lifecycle

// 1. Create cardholder
// 2. Issue card (virtual or physical)
// 3. Activate card (physical only)
// 4. Monitor transactions
// 5. Update/cancel card as needed

Real-time Authorization

// Webhook: issuing_authorization.request
// → Check rules
// → Approve or decline within 5 seconds

Search Helpers

# Find Terminal docs
grep -r "Terminal\|reader\|in-person" /Users/zach/Documents/cc-skills/docs/stripe/terminal/

# Find Issuing docs
grep -r "Issuing\|card\|authorization" /Users/zach/Documents/cc-skills/docs/stripe/issuing/

# List Terminal files
ls /Users/zach/Documents/cc-skills/docs/stripe/terminal/

# List Issuing files
ls /Users/zach/Documents/cc-skills/docs/stripe/issuing/

Common Errors

Terminal

  • Reader not found: Reader not registered or offline

    • Solution: Check reader power and connection
  • Connection token expired: Token older than 60 seconds

    • Solution: Fetch new connection token
  • Payment timeout: Reader timeout waiting for card

    • Solution: Set appropriate timeout or cancel

Issuing

  • Cardholder required: Missing cardholder

    • Solution: Create cardholder first
  • Spending limit exceeded: Transaction over limit

    • Solution: Adjust spending controls
  • Authorization declined: Custom logic declined

    • Solution: Check authorization rules

Security Notes

Terminal

  • Secure connection tokens - Use HTTPS
  • Validate reader ID - Ensure reader belongs to your account
  • Monitor offline mode - Reconcile offline payments

Issuing

  • Protect card details - Never log full card numbers
  • Implement fraud detection - Monitor unusual patterns
  • Use real-time authorizations - Control spending instantly
  • Secure cardholder data - Follow PCI compliance

Notes

  • Documentation covers latest Stripe API (2023+)
  • Terminal requires physical hardware (readers)
  • Issuing requires approval from Stripe
  • Real-time authorization webhook must respond within 5 seconds
  • Virtual cards are instant, physical cards take 3-7 days
  • File paths reference local documentation cache
  • For latest updates, check: